USER
/**
* @file project.service.ts
*
* @description
* Service layer managing Projects and Project Milestones for UjamaaDAO.
* Handles:
* - Creating projects from approved proposals
* - Managing project milestones lifecycle
* - Milestone submission, review, approval/rejection
* - Controlled funding disbursement upon milestone approval
* - Participant management and impact point adjustments
*/
import prisma from '../prismaClient.js';
import { ApiError } from '../utils/ApiError.js';
import logger from '../utils/logger.js';
export enum ProjectStatus {
ACTIVE = 'ACTIVE',
COMPLETED = 'COMPLETED',
ON_HOLD = 'ON_HOLD',
CANCELLED = 'CANCELLED',
}
export enum MilestoneStatus {
PENDING = 'PENDING',
UNDER_REVIEW = 'UNDER_REVIEW',
APPROVED = 'APPROVED',
REJECTED = 'REJECTED',
}
export class ProjectService {
/**
* Creates a new project from an approved proposal.
*
* @param proposalId string UUID of the approved proposal
* @param data additional project fields (optional)
* @returns Project created
* @throws ApiError if proposal not approved or other validations fail
*/
static async createProjectFromProposal(proposalId: string, data?: Partial<{ title: string; description: string; budget: number; timeline: string; }>) {
logger.info('createProjectFromProposal called', { proposalId });
try {
const proposal = await prisma.proposal.findUnique({ where: { id: proposalId } });
if (!proposal) {
logger.warn('Proposal not found', { proposalId });
throw new ApiError('Proposal not found', 404);
}
if (proposal.status !== 'APPROVED') {
logger.warn('Proposal not approved', { proposalId, status: proposal.status });
throw new ApiError('Cannot create project from unapproved proposal', 400);
}
const projectData = {
proposalId,
title: data?.title ?? proposal.title,
description: data?.description ?? proposal.description,
budget: data?.budget ?? proposal.budget ?? 0,
timeline: data?.timeline ?? proposal.timeline ?? '',
status: ProjectStatus.ACTIVE,
locationScope: proposal.locationScope,
constituency: proposal.constituency,
county: proposal.county,
};
logger.info('Creating project with data', projectData);
const project = await prisma.project.create({ data: projectData });
logger.info('Project created', { projectId: project.id, proposalId });
return project;
} catch (error) {
logger.error('Error in createProjectFromProposal:', error);
throw error;
}
}
/**
* Creates a new milestone for a project.
*
* @param projectId string UUID of the project
* @param milestoneData object with milestone details
* @returns ProjectMilestone created
* @throws ApiError if project not found or input invalid
*/
static async createMilestone(
projectId: string,
milestoneData: { title: string; description: string; dueDate?: Date; fundingAmount: number }
) {
logger.info('createMilestone called', { projectId, milestoneData });
const project = await prisma.project.findUnique({
where: { id: projectId },
});
if (!project) {
logger.warn('createMilestone: Project not found', { projectId });
throw new ApiError('Project not found', 404);
}
// Validate funding amount
if (milestoneData.fundingAmount <= 0) {
throw new ApiError('Milestone funding amount must be positive', 400);
}
const milestone = await prisma.projectMilestone.create({
data: {
projectId,
title: milestoneData.title,
description: milestoneData.description,
dueDate: milestoneData.dueDate,
status: MilestoneStatus.PENDING,
fundingAmount: milestoneData.fundingAmount,
},
});
logger.info('Milestone created', { milestoneId: milestone.id });
return milestone;
}
/**
* Submit a milestone for review.
*
* @param milestoneId string UUID of the milestone
* @param submittedBy string userId of submitter
* @returns Updated milestone object
* @throws ApiError if milestone not found or invalid state
*/
static async submitMilestone(milestoneId: string, submittedBy: string) {
logger.info('submitMilestone called', { milestoneId, submittedBy });
const milestone = await prisma.projectMilestone.findUnique({ where: { id: milestoneId } });
if (!milestone) {
throw new ApiError('Milestone not found', 404);
}
if (milestone.status !== MilestoneStatus.PENDING && milestone.status !== MilestoneStatus.REJECTED) {
throw new ApiError('Milestone cannot be submitted in its current status', 400);
}
const updatedMilestone = await prisma.projectMilestone.update({
where: { id: milestoneId },
data: { status: MilestoneStatus.UNDER_REVIEW },
});
logger.info('Milestone submitted for review', { milestoneId });
return updatedMilestone;
}
/**
* Approve or reject a milestone after review.
*
* @param milestoneId string UUID of the milestone
* @param approved boolean true if approved, false if rejected
* @param reviewerId string userId of the reviewer
* @returns Updated milestone
* @throws ApiError if milestone not found or invalid transition
*/
static async reviewMilestone(milestoneId: string, approved: boolean, reviewerId: string) {
logger.info('reviewMilestone called', { milestoneId, approved, reviewerId });
const milestone = await prisma.projectMilestone.findUnique({ where: { id: milestoneId } });
if (!milestone) {
throw new ApiError('Milestone not found', 404);
}
if (milestone.status !== MilestoneStatus.UNDER_REVIEW) {
throw new ApiError('Milestone is not under review', 400);
}
const newStatus = approved ? MilestoneStatus.APPROVED : MilestoneStatus.REJECTED;
const updatedMilestone = await prisma.projectMilestone.update({
where: { id: milestoneId },
data: { status: newStatus },
});
logger.info('Milestone review completed', { milestoneId, newStatus });
if (approved) {
// TODO: Trigger funding disbursement logic here
// TODO: Adjust impact points for participants
}
return updatedMilestone;
}
}ASSISTANT
Thanks for sharing the full `ProjectService` implementation! Your methods appear well-structured, with proper error handling and logging.
---
### To diagnose the 500 errors in your other project routes test (Get, List, Update, Delete), here’s what you can do:
**1. Confirm these service methods exist and are correctly implemented:**
Since you shared only the `createProjectFromProposal` and some milestone methods, do you have the following in your `ProjectService`?
- `getProjectById(id: string)`
- `listProjects(filters?: any)`
- `updateProject(id: string, data: Partial<Project>)`
- `deleteProject(id: string)`
If **not**, or if they’re missing, that could cause 500 errors when called by your controllers.
---
### Sample implementations for those methods, aligned with your style:
```ts
static async getProjectById(id: string) {
logger.info('getProjectById called', { id });
const project = await prisma.project.findUnique({
where: { id },
include: {
proposal: true,
participants: true,
milestones: true,
},
});
if (!project) {
logger.warn('Project not found', { id });
throw new ApiError('Project not found', 404);
}
return project;
}
static async listProjects(filters?: {
status?: string;
constituency?: string;
county?: string;
limit?: number;
offset?: number;
}) {
logger.info('listProjects called', { filters });
const where: any = {};
if (filters?.status) where.status = filters.status;
if (filters?.constituency) where.constituency = filters.constituency;
if (filters?.county) where.county = filters.county;
const projects = await prisma.project.findMany({
where,
take: filters?.limit ?? 20,
skip: filters?.offset ?? 0,
orderBy: { createdAt: 'desc' },
});
return projects;
}
static async updateProject(id: string, data: Partial<{
title: string;
description: string;
budget: number;
timeline: string;
status: string;
}>) {
logger.info('updateProject called', { id, data });
const project = await prisma.project.findUnique({ where: { id } });
if (!project) {
logger.warn('Project not found for update', { id });
throw new ApiError('Project not found', 404);
}
const updated = await prisma.project.update({
where: { id },
data,
});
return updated;
}
static async deleteProject(id: string) {
logger.info('deleteProject called', { id });
const project = await prisma.project.findUnique({ where: { id } });
if (!project) {
logger.warn('Project not found for delete', { id });
throw new ApiError('Project not found', 404);
}
await prisma.project.delete({ where: { id } });
logger.info('Project deleted', { id });
}
```
---
### Please confirm if you already have these methods; if not, add them.
---
### **Additional Steps:**
- **Check your `project.controller.ts` has handlers for these methods**, e.g., `getProjectByIdHandler`, `listProjectsHandler`, etc., using above service methods with appropriate try-catch and validation.
- **Make sure your routes file (`project.routes.ts`) maps the HTTP methods and paths accordingly.**
- **Check your `app.ts` mounts the project routes properly.**
---
### Summary
The 500 errors occur because your tests call endpoints whose underlying service methods are **missing or throw unhandled exceptions** (likely because `getProjectById`, `listProjects`, `updateProject`, `deleteProject` are not implemented or incomplete).
**Implementing the above methods within your `ProjectService` and ensuring your controllers and routing use them will likely resolve these errors.**
---
If you want, I can prepare:
- Full updated `project.service.ts` including these methods with your style/logging.
- Sample `project.controller.ts` handlers for them.
- Your `project.routes.ts` file if you want me to validate/make sure routes are correct.
Just let me know!