feat: add application layer

- Implement TaskService with business logic
- Implement StateService with business logic
This commit is contained in:
Andres Fabian Patiño Bermudez 2026-05-14 21:32:42 -05:00
parent 7bf617bd4b
commit 8eafb8ee81
2 changed files with 147 additions and 0 deletions

View File

@ -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();
}
}

View File

@ -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;
}
}