feat: add application layer
- Implement TaskService with business logic - Implement StateService with business logic
This commit is contained in:
parent
7bf617bd4b
commit
8eafb8ee81
|
|
@ -0,0 +1,10 @@
|
||||||
|
import type { State } from "../../domain/state/state.entity.js";
|
||||||
|
import type { StateRepository } from "../../domain/state/state.repository.js";
|
||||||
|
|
||||||
|
export class StateService {
|
||||||
|
constructor(private readonly stateRepo: StateRepository) {}
|
||||||
|
|
||||||
|
async getAll(): Promise<State[]> {
|
||||||
|
return this.stateRepo.findAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
import type { Task, TaskState } from "../../domain/task/task.entity.js";
|
||||||
|
import type { TaskRepository } from "../../domain/task/task.repository.js";
|
||||||
|
import type { StateRepository } from "../../domain/state/state.repository.js";
|
||||||
|
import type { CreateTaskDto, UpdateTaskDto, AddNoteDto } from "../../shared/dto.js";
|
||||||
|
import { AppError } from "../../shared/app-error.js";
|
||||||
|
|
||||||
|
const VALID_TRANSITIONS: Record<TaskState, TaskState[]> = {
|
||||||
|
new: ["active"],
|
||||||
|
active: ["resolved", "closed"],
|
||||||
|
resolved: ["closed", "active"],
|
||||||
|
closed: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export class TaskService {
|
||||||
|
constructor(
|
||||||
|
private readonly taskRepo: TaskRepository,
|
||||||
|
private readonly stateRepo: StateRepository
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getAll(): Promise<Task[]> {
|
||||||
|
return this.taskRepo.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getById(id: string): Promise<Task> {
|
||||||
|
const task = await this.taskRepo.findById(id);
|
||||||
|
if (!task) {
|
||||||
|
throw new AppError(404, `Task with id '${id}' not found`);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(dto: CreateTaskDto): Promise<Task> {
|
||||||
|
const now = new Date().toISOString().split("T")[0];
|
||||||
|
|
||||||
|
const newTask: Task = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
title: dto.title,
|
||||||
|
description: dto.description,
|
||||||
|
dueDate: dto.dueDate,
|
||||||
|
completed: false,
|
||||||
|
deletedAt: null,
|
||||||
|
stateHistory: [{ state: "new", date: now }],
|
||||||
|
notes: dto.notes ?? [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.taskRepo.create(newTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, dto: UpdateTaskDto): Promise<Task> {
|
||||||
|
await this.getById(id);
|
||||||
|
|
||||||
|
const updated = await this.taskRepo.update(id, {
|
||||||
|
...(dto.title !== undefined && { title: dto.title }),
|
||||||
|
...(dto.description !== undefined && { description: dto.description }),
|
||||||
|
...(dto.dueDate !== undefined && { dueDate: dto.dueDate }),
|
||||||
|
...(dto.completed !== undefined && { completed: dto.completed }),
|
||||||
|
...(dto.notes !== undefined && { notes: dto.notes }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
throw new AppError(500, "Failed to update task");
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
await this.getById(id);
|
||||||
|
const deleted = await this.taskRepo.delete(id);
|
||||||
|
if (!deleted) {
|
||||||
|
throw new AppError(500, "Failed to delete task");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async transitionState(id: string, newState: TaskState): Promise<Task> {
|
||||||
|
const task = await this.getById(id);
|
||||||
|
const currentState = task.stateHistory[task.stateHistory.length - 1]?.state;
|
||||||
|
|
||||||
|
if (!currentState) {
|
||||||
|
throw new AppError(500, "Task has no state history");
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowed = VALID_TRANSITIONS[currentState];
|
||||||
|
if (!allowed.includes(newState)) {
|
||||||
|
throw new AppError(400, `Cannot transition from '${currentState}' to '${newState}'. Allowed: ${allowed.join(", ") || "none"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validState = await this.stateRepo.findByName(newState);
|
||||||
|
if (!validState) {
|
||||||
|
throw new AppError(400, `Invalid state '${newState}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date().toISOString().split("T")[0];
|
||||||
|
const updated = await this.taskRepo.update(id, {
|
||||||
|
stateHistory: [...task.stateHistory, { state: newState, date: now }],
|
||||||
|
completed: newState === "closed",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
throw new AppError(500, "Failed to transition task state");
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addNote(id: string, dto: AddNoteDto): Promise<Task> {
|
||||||
|
const task = await this.getById(id);
|
||||||
|
|
||||||
|
const updated = await this.taskRepo.update(id, {
|
||||||
|
notes: [...task.notes, dto.note],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
throw new AppError(500, "Failed to add note");
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteNote(id: string, noteIndex: number): Promise<Task> {
|
||||||
|
const task = await this.getById(id);
|
||||||
|
|
||||||
|
if (noteIndex < 0 || noteIndex >= task.notes.length) {
|
||||||
|
throw new AppError(400, `Invalid note index ${noteIndex}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const notes = [...task.notes];
|
||||||
|
notes.splice(noteIndex, 1);
|
||||||
|
|
||||||
|
const updated = await this.taskRepo.update(id, { notes });
|
||||||
|
if (!updated) {
|
||||||
|
throw new AppError(500, "Failed to delete note");
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue