From b79f524ef5d33896144ae88d00100007b9e33ba9 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 16:26:59 +0000 Subject: [PATCH 1/4] add deadline --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4a80115..19aec56 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/TvkQWWs6) # Features of modern Java # Цели и задачи л/р: From 2fca582efbbdff39bdf2f19bdd00c2a30f8d17dd Mon Sep 17 00:00:00 2001 From: Vladislav Bandurin Date: Wed, 14 Jan 2026 19:10:36 +0300 Subject: [PATCH 2/4] complete lab4 --- .idea/.gitignore | 3 + .idea/.name | 1 + .idea/gradle.xml | 15 ++++ .idea/inspectionProfiles/Project_Default.xml | 6 ++ .idea/misc.xml | 5 ++ .idea/vcs.xml | 6 ++ .java-version | 1 + build.gradle.kts | 30 +++++-- gradlew | 0 src/main/java/org/lab/Main.java | 84 ++++++++++++++++++- src/main/java/org/lab/model/BugReport.java | 36 ++++++++ src/main/java/org/lab/model/BugStatus.java | 8 ++ src/main/java/org/lab/model/Milestone.java | 53 ++++++++++++ .../java/org/lab/model/MilestoneStatus.java | 5 ++ src/main/java/org/lab/model/Project.java | 32 +++++++ .../java/org/lab/model/ProjectMembership.java | 5 ++ src/main/java/org/lab/model/Role.java | 8 ++ src/main/java/org/lab/model/Ticket.java | 42 ++++++++++ src/main/java/org/lab/model/TicketStatus.java | 5 ++ src/main/java/org/lab/model/User.java | 5 ++ .../lab/repository/BugReportRepository.java | 26 ++++++ .../lab/repository/MilestoneRepository.java | 30 +++++++ .../org/lab/repository/ProjectRepository.java | 29 +++++++ .../org/lab/repository/TicketRepository.java | 36 ++++++++ .../org/lab/repository/UserRepository.java | 28 +++++++ .../org/lab/service/BugReportService.java | 78 +++++++++++++++++ .../org/lab/service/MilestoneService.java | 73 ++++++++++++++++ .../java/org/lab/service/ProjectService.java | 46 ++++++++++ .../lab/service/RoleValidationService.java | 23 +++++ .../java/org/lab/service/TicketService.java | 82 ++++++++++++++++++ .../java/org/lab/service/UserService.java | 51 +++++++++++ .../java/org/lab/service/WorkflowService.java | 78 +++++++++++++++++ src/main/java/org/lab/workflow/Action.java | 13 +++ .../org/lab/workflow/AssignDeveloper.java | 9 ++ src/main/java/org/lab/workflow/CloseBug.java | 5 ++ .../java/org/lab/workflow/CompleteWork.java | 5 ++ .../java/org/lab/workflow/CreateTicket.java | 8 ++ src/main/java/org/lab/workflow/FixBug.java | 5 ++ src/main/java/org/lab/workflow/ReportBug.java | 7 ++ src/main/java/org/lab/workflow/StartWork.java | 5 ++ src/main/java/org/lab/workflow/TestBug.java | 5 ++ .../org/lab/service/BugReportServiceTest.java | 41 +++++++++ .../org/lab/service/MilestoneServiceTest.java | 46 ++++++++++ .../org/lab/service/ProjectServiceTest.java | 48 +++++++++++ .../org/lab/service/TicketServiceTest.java | 52 ++++++++++++ 45 files changed, 1171 insertions(+), 8 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/gradle.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 .java-version mode change 100644 => 100755 gradlew create mode 100644 src/main/java/org/lab/model/BugReport.java create mode 100644 src/main/java/org/lab/model/BugStatus.java create mode 100644 src/main/java/org/lab/model/Milestone.java create mode 100644 src/main/java/org/lab/model/MilestoneStatus.java create mode 100644 src/main/java/org/lab/model/Project.java create mode 100644 src/main/java/org/lab/model/ProjectMembership.java create mode 100644 src/main/java/org/lab/model/Role.java create mode 100644 src/main/java/org/lab/model/Ticket.java create mode 100644 src/main/java/org/lab/model/TicketStatus.java create mode 100644 src/main/java/org/lab/model/User.java create mode 100644 src/main/java/org/lab/repository/BugReportRepository.java create mode 100644 src/main/java/org/lab/repository/MilestoneRepository.java create mode 100644 src/main/java/org/lab/repository/ProjectRepository.java create mode 100644 src/main/java/org/lab/repository/TicketRepository.java create mode 100644 src/main/java/org/lab/repository/UserRepository.java create mode 100644 src/main/java/org/lab/service/BugReportService.java create mode 100644 src/main/java/org/lab/service/MilestoneService.java create mode 100644 src/main/java/org/lab/service/ProjectService.java create mode 100644 src/main/java/org/lab/service/RoleValidationService.java create mode 100644 src/main/java/org/lab/service/TicketService.java create mode 100644 src/main/java/org/lab/service/UserService.java create mode 100644 src/main/java/org/lab/service/WorkflowService.java create mode 100644 src/main/java/org/lab/workflow/Action.java create mode 100644 src/main/java/org/lab/workflow/AssignDeveloper.java create mode 100644 src/main/java/org/lab/workflow/CloseBug.java create mode 100644 src/main/java/org/lab/workflow/CompleteWork.java create mode 100644 src/main/java/org/lab/workflow/CreateTicket.java create mode 100644 src/main/java/org/lab/workflow/FixBug.java create mode 100644 src/main/java/org/lab/workflow/ReportBug.java create mode 100644 src/main/java/org/lab/workflow/StartWork.java create mode 100644 src/main/java/org/lab/workflow/TestBug.java create mode 100644 src/test/java/org/lab/service/BugReportServiceTest.java create mode 100644 src/test/java/org/lab/service/MilestoneServiceTest.java create mode 100644 src/test/java/org/lab/service/ProjectServiceTest.java create mode 100644 src/test/java/org/lab/service/TicketServiceTest.java diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..0194542 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +features \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..f9163b4 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..ab89d8f --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..313aec6 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.java-version b/.java-version new file mode 100644 index 0000000..aabe6ec --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +21 diff --git a/build.gradle.kts b/build.gradle.kts index 79bf52a..9bd800a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,20 +1,40 @@ plugins { - id("java") + java + application } group = "org.lab" version = "1.0-SNAPSHOT" +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } +} + repositories { mavenCentral() } +application { + mainClass.set("org.lab.Main") +} + dependencies { - testImplementation(platform("org.junit:junit-bom:5.10.0")) - testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.test { +tasks.withType().configureEach { + options.compilerArgs.add("--enable-preview") +} + +tasks.withType().configureEach { useJUnitPlatform() -} \ No newline at end of file + jvmArgs("--enable-preview") +} + +tasks.withType().configureEach { + jvmArgs("--enable-preview") +} diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/org/lab/Main.java b/src/main/java/org/lab/Main.java index 22028ef..37f556d 100644 --- a/src/main/java/org/lab/Main.java +++ b/src/main/java/org/lab/Main.java @@ -1,4 +1,82 @@ -void main() { - IO.println("Hello and welcome!"); -} +package org.lab; + +import org.lab.model.*; +import org.lab.repository.*; +import org.lab.service.*; +import org.lab.workflow.*; + +import java.time.LocalDate; + +public class Main { + public static void main() { + + UserRepository userRepo = new UserRepository(); + ProjectRepository projectRepo = new ProjectRepository(); + MilestoneRepository milestoneRepo = new MilestoneRepository(); + TicketRepository ticketRepo = new TicketRepository(); + BugReportRepository bugRepo = new BugReportRepository(); + + RoleValidationService roleValidator = new RoleValidationService(); + + UserService userService = new UserService(userRepo, projectRepo, ticketRepo, bugRepo); + ProjectService projectService = new ProjectService(projectRepo, roleValidator); + MilestoneService milestoneService = new MilestoneService(milestoneRepo, ticketRepo, roleValidator); + TicketService ticketService = new TicketService(ticketRepo, roleValidator); + BugReportService bugService = new BugReportService(bugRepo, roleValidator); + + WorkflowService workflow = new WorkflowService(ticketService, bugService); + + User manager = userService.register("Alice (Manager)"); + User dev = userService.register("Bob (Developer)"); + User tester = userService.register("Eve (Tester)"); + + Project project = projectService.createProject(manager, "Enterprise Project"); + project = projectService.addMember(project, manager, dev, Role.DEVELOPER); + project = projectService.addMember(project, manager, tester, Role.TESTER); + + Milestone milestone = milestoneService.createMilestone(manager, project, "Milestone 1", + LocalDate.now(), LocalDate.now().plusDays(14)); + Ticket ticket = workflow.apply(manager, project, milestone, + new CreateTicket("Implement Auth", "JWT-based login/logout")); + + ticket = workflow.apply(manager, project, ticket, + new AssignDeveloper(dev.id())); + + ticket = workflow.apply(dev, project, ticket, new StartWork()); + ticket = workflow.apply(dev, project, ticket, new CompleteWork()); + + System.out.println("Ticket status after completion: " + ticket.status()); + + BugReport bug = workflow.apply(tester, project, null, + new ReportBug("Login fails on empty password")); + + bug = workflow.apply(dev, project, bug, new FixBug()); + bug = workflow.apply(tester, project, bug, new TestBug()); + bug = workflow.apply(tester, project, bug, new CloseBug()); + + System.out.println("Bug status after closure: " + bug.status()); + + milestone = milestoneService.closeMilestone(manager, project, milestone); + + String report = """ + ===== PROJECT REPORT ===== + Project: %s + Milestone: %s + Milestone status: %s + Ticket: %s + Ticket status: %s + Bug status: %s + ========================= + """.formatted( + project.name(), + milestone.name(), + milestone.status(), + ticket.title(), + ticket.status(), + bug.status() + ); + + System.out.println(report); + } +} diff --git a/src/main/java/org/lab/model/BugReport.java b/src/main/java/org/lab/model/BugReport.java new file mode 100644 index 0000000..a05c78f --- /dev/null +++ b/src/main/java/org/lab/model/BugReport.java @@ -0,0 +1,36 @@ +package org.lab.model; + +import java.util.UUID; + +public record BugReport( + UUID id, + UUID projectId, + String description, + BugStatus status, + UUID assignedDeveloperId +) { + + public BugReport assignDeveloper(UUID developerId) { + return new BugReport( + id, + projectId, + description, + status, + developerId + ); + } + + public BugReport changeStatus(BugStatus newStatus) { + return new BugReport( + id, + projectId, + description, + newStatus, + assignedDeveloperId + ); + } + + public boolean isOpen() { + return status != BugStatus.CLOSED; + } +} diff --git a/src/main/java/org/lab/model/BugStatus.java b/src/main/java/org/lab/model/BugStatus.java new file mode 100644 index 0000000..71e551d --- /dev/null +++ b/src/main/java/org/lab/model/BugStatus.java @@ -0,0 +1,8 @@ +package org.lab.model; + +public enum BugStatus { + NEW, + FIXED, + TESTED, + CLOSED +} diff --git a/src/main/java/org/lab/model/Milestone.java b/src/main/java/org/lab/model/Milestone.java new file mode 100644 index 0000000..6239cd1 --- /dev/null +++ b/src/main/java/org/lab/model/Milestone.java @@ -0,0 +1,53 @@ +package org.lab.model; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +public record Milestone( + UUID id, + UUID projectId, + String name, + LocalDate startDate, + LocalDate endDate, + MilestoneStatus status, + List ticketIds +) { + + public Milestone addTicket(UUID ticketId) { + var updated = new java.util.ArrayList<>(ticketIds); + updated.add(ticketId); + + return new Milestone( + id, + projectId, + name, + startDate, + endDate, + status, + List.copyOf(updated) + ); + } + + public boolean canBeClosed(List tickets) { + return tickets.stream() + .filter(t -> ticketIds.contains(t.id())) + .allMatch(Ticket::isCompleted); + } + + public Milestone changeStatus(MilestoneStatus newStatus) { + return new Milestone( + id, + projectId, + name, + startDate, + endDate, + newStatus, + ticketIds + ); + } + + public boolean isActive() { + return status == MilestoneStatus.ACTIVE; + } +} diff --git a/src/main/java/org/lab/model/MilestoneStatus.java b/src/main/java/org/lab/model/MilestoneStatus.java new file mode 100644 index 0000000..48a0334 --- /dev/null +++ b/src/main/java/org/lab/model/MilestoneStatus.java @@ -0,0 +1,5 @@ +package org.lab.model; + +public enum MilestoneStatus { + OPEN, ACTIVE, CLOSED +} diff --git a/src/main/java/org/lab/model/Project.java b/src/main/java/org/lab/model/Project.java new file mode 100644 index 0000000..ab69f9f --- /dev/null +++ b/src/main/java/org/lab/model/Project.java @@ -0,0 +1,32 @@ +package org.lab.model; + +import java.util.*; + +public record Project( + UUID id, + String name, + List memberships, + List milestones, + List bugReports +) { + + public Project addMember(UUID userId, Role role) { + var updated = new ArrayList<>(memberships); + updated.add(new ProjectMembership(userId, role)); + + return new Project( + id, + name, + List.copyOf(updated), + milestones, + bugReports + ); + } + + public Optional getUserRole(UUID userId) { + return memberships.stream() + .filter(m -> m.userId().equals(userId)) + .map(ProjectMembership::role) + .findFirst(); + } +} diff --git a/src/main/java/org/lab/model/ProjectMembership.java b/src/main/java/org/lab/model/ProjectMembership.java new file mode 100644 index 0000000..b0ab3dc --- /dev/null +++ b/src/main/java/org/lab/model/ProjectMembership.java @@ -0,0 +1,5 @@ +package org.lab.model; + +import java.util.UUID; + +public record ProjectMembership(UUID userId, Role role) {} diff --git a/src/main/java/org/lab/model/Role.java b/src/main/java/org/lab/model/Role.java new file mode 100644 index 0000000..a14363f --- /dev/null +++ b/src/main/java/org/lab/model/Role.java @@ -0,0 +1,8 @@ +package org.lab.model; + +public enum Role { + MANAGER, + TEAM_LEAD, + DEVELOPER, + TESTER +} diff --git a/src/main/java/org/lab/model/Ticket.java b/src/main/java/org/lab/model/Ticket.java new file mode 100644 index 0000000..40f7615 --- /dev/null +++ b/src/main/java/org/lab/model/Ticket.java @@ -0,0 +1,42 @@ +package org.lab.model; + +import java.util.UUID; + +public record Ticket( + UUID id, + UUID projectId, + UUID milestoneId, + String title, + String description, + TicketStatus status, + UUID assignedDeveloperId +) { + + public Ticket assignDeveloper(UUID developerId) { + return new Ticket( + id, + projectId, + milestoneId, + title, + description, + status, + developerId + ); + } + + public Ticket changeStatus(TicketStatus newStatus) { + return new Ticket( + id, + projectId, + milestoneId, + title, + description, + newStatus, + assignedDeveloperId + ); + } + + public boolean isCompleted() { + return status == TicketStatus.DONE; + } +} diff --git a/src/main/java/org/lab/model/TicketStatus.java b/src/main/java/org/lab/model/TicketStatus.java new file mode 100644 index 0000000..fc853de --- /dev/null +++ b/src/main/java/org/lab/model/TicketStatus.java @@ -0,0 +1,5 @@ +package org.lab.model; + +public enum TicketStatus { + NEW, ACCEPTED, IN_PROGRESS, DONE +} diff --git a/src/main/java/org/lab/model/User.java b/src/main/java/org/lab/model/User.java new file mode 100644 index 0000000..1de13a5 --- /dev/null +++ b/src/main/java/org/lab/model/User.java @@ -0,0 +1,5 @@ +package org.lab.model; + +import java.util.UUID; + +public record User(UUID id, String name) {} diff --git a/src/main/java/org/lab/repository/BugReportRepository.java b/src/main/java/org/lab/repository/BugReportRepository.java new file mode 100644 index 0000000..8a5a9f8 --- /dev/null +++ b/src/main/java/org/lab/repository/BugReportRepository.java @@ -0,0 +1,26 @@ +package org.lab.repository; + +import org.lab.model.BugReport; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class BugReportRepository { + + private final Map storage = new ConcurrentHashMap<>(); + + public BugReport save(BugReport bug) { + storage.put(bug.id(), bug); + return bug; + } + + public List findAll() { + return List.copyOf(storage.values()); + } + + public List findByProject(UUID projectId) { + return storage.values().stream() + .filter(b -> b.projectId().equals(projectId)) + .toList(); + } +} diff --git a/src/main/java/org/lab/repository/MilestoneRepository.java b/src/main/java/org/lab/repository/MilestoneRepository.java new file mode 100644 index 0000000..091e694 --- /dev/null +++ b/src/main/java/org/lab/repository/MilestoneRepository.java @@ -0,0 +1,30 @@ +package org.lab.repository; + +import org.lab.model.Milestone; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class MilestoneRepository { + + private final Map storage = new ConcurrentHashMap<>(); + + public Milestone save(Milestone milestone) { + storage.put(milestone.id(), milestone); + return milestone; + } + + public Optional findById(UUID id) { + return Optional.ofNullable(storage.get(id)); + } + + public List findAll() { + return List.copyOf(storage.values()); + } + + public List findByProject(UUID projectId) { + return storage.values().stream() + .filter(m -> m.projectId().equals(projectId)) + .toList(); + } +} diff --git a/src/main/java/org/lab/repository/ProjectRepository.java b/src/main/java/org/lab/repository/ProjectRepository.java new file mode 100644 index 0000000..68e79ea --- /dev/null +++ b/src/main/java/org/lab/repository/ProjectRepository.java @@ -0,0 +1,29 @@ +package org.lab.repository; + +import org.lab.model.Project; + +import java.util.*; +import java.util.stream.Collectors; + +public class ProjectRepository { + private final Map storage = new HashMap<>(); + + public Project save(Project project) { + storage.put(project.id(), project); + return project; + } + + public Optional findById(UUID id) { + return Optional.ofNullable(storage.get(id)); + } + + public List findAll() { + return new ArrayList<>(storage.values()); + } + + public List findByUser(UUID userId) { + return storage.values().stream() + .filter(p -> p.memberships().stream().anyMatch(u -> u.userId().equals(userId))) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/org/lab/repository/TicketRepository.java b/src/main/java/org/lab/repository/TicketRepository.java new file mode 100644 index 0000000..cf62098 --- /dev/null +++ b/src/main/java/org/lab/repository/TicketRepository.java @@ -0,0 +1,36 @@ +package org.lab.repository; + +import org.lab.model.Ticket; +import org.lab.model.TicketStatus; + +import java.util.*; +import java.util.stream.Collectors; + +public class TicketRepository { + private final Map storage = new HashMap<>(); + + public Ticket save(Ticket ticket) { + storage.put(ticket.id(), ticket); + return ticket; + } + + public Optional findById(UUID id) { + return Optional.ofNullable(storage.get(id)); + } + + public List findAll() { + return new ArrayList<>(storage.values()); + } + + public List findByDeveloper(UUID developerId) { + return storage.values().stream() + .filter(t -> t.assignedDeveloperId() != null && t.assignedDeveloperId().equals(developerId)) + .collect(Collectors.toList()); + } + + public List findByStatus(TicketStatus status) { + return storage.values().stream() + .filter(t -> t.status() == status) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/org/lab/repository/UserRepository.java b/src/main/java/org/lab/repository/UserRepository.java new file mode 100644 index 0000000..48d7568 --- /dev/null +++ b/src/main/java/org/lab/repository/UserRepository.java @@ -0,0 +1,28 @@ +package org.lab.repository; + +import org.lab.model.User; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class UserRepository { + + private final Map users = new ConcurrentHashMap<>(); + + public User save(User user) { + users.put(user.id(), user); + return user; + } + + public Optional findById(UUID id) { + return Optional.ofNullable(users.get(id)); + } + + public List findAll() { + return List.copyOf(users.values()); + } + + public boolean existsById(UUID id) { + return users.containsKey(id); + } +} diff --git a/src/main/java/org/lab/service/BugReportService.java b/src/main/java/org/lab/service/BugReportService.java new file mode 100644 index 0000000..e718779 --- /dev/null +++ b/src/main/java/org/lab/service/BugReportService.java @@ -0,0 +1,78 @@ +package org.lab.service; + +import org.lab.model.*; +import org.lab.repository.BugReportRepository; + +import java.util.UUID; + +public class BugReportService { + + private final BugReportRepository repository; + private final RoleValidationService roleValidator; + + public BugReportService( + BugReportRepository repository, + RoleValidationService roleValidator + ) { + this.repository = repository; + this.roleValidator = roleValidator; + } + + public BugReport createBug( + User user, + Project project, + String description + ) { + roleValidator.requireRole(project, user, Role.DEVELOPER, Role.TESTER); + + BugReport bug = new BugReport( + UUID.randomUUID(), + project.id(), + description, + BugStatus.NEW, + null + ); + + repository.save(bug); + return bug; + } + + public BugReport fixBug( + User developer, + Project project, + BugReport bug + ) { + roleValidator.requireRole(project, developer, Role.DEVELOPER); + + BugReport updated = bug + .assignDeveloper(developer.id()) + .changeStatus(BugStatus.FIXED); + + repository.save(updated); + return updated; + } + + public BugReport testBug( + User tester, + Project project, + BugReport bug + ) { + roleValidator.requireRole(project, tester, Role.TESTER); + + BugReport updated = bug.changeStatus(BugStatus.TESTED); + repository.save(updated); + return updated; + } + + public BugReport closeBug( + User tester, + Project project, + BugReport bug + ) { + roleValidator.requireRole(project, tester, Role.TESTER); + + BugReport updated = bug.changeStatus(BugStatus.CLOSED); + repository.save(updated); + return updated; + } +} \ No newline at end of file diff --git a/src/main/java/org/lab/service/MilestoneService.java b/src/main/java/org/lab/service/MilestoneService.java new file mode 100644 index 0000000..0ff0647 --- /dev/null +++ b/src/main/java/org/lab/service/MilestoneService.java @@ -0,0 +1,73 @@ +package org.lab.service; + +import org.lab.model.*; +import org.lab.repository.MilestoneRepository; +import org.lab.repository.TicketRepository; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +public class MilestoneService { + + private final MilestoneRepository milestoneRepo; + private final TicketRepository ticketRepo; + private final RoleValidationService roleValidator; + + public MilestoneService( + MilestoneRepository milestoneRepo, + TicketRepository ticketRepo, + RoleValidationService roleValidator + ) { + this.milestoneRepo = milestoneRepo; + this.ticketRepo = ticketRepo; + this.roleValidator = roleValidator; + } + + public Milestone createMilestone( + User manager, + Project project, + String name, + LocalDate start, + LocalDate end + ) { + roleValidator.requireRole(project, manager, Role.MANAGER); + + boolean activeExists = milestoneRepo.findByProject(project.id()).stream() + .anyMatch(m -> m.status() == MilestoneStatus.ACTIVE); + + if (activeExists) { + throw new IllegalStateException("Активный майлстоун уже существует"); + } + + Milestone milestone = new Milestone( + UUID.randomUUID(), + project.id(), + name, + start, + end, + MilestoneStatus.ACTIVE, + List.of() + ); + + milestoneRepo.save(milestone); + return milestone; + } + + public Milestone closeMilestone( + User manager, + Project project, + Milestone milestone + ) { + roleValidator.requireRole(project, manager, Role.MANAGER); + + if (!milestone.canBeClosed(ticketRepo.findAll())) { + throw new IllegalStateException("Есть невыполненные тикеты"); + } + + Milestone closed = milestone.changeStatus(MilestoneStatus.CLOSED); + milestoneRepo.save(closed); + return closed; + } +} + diff --git a/src/main/java/org/lab/service/ProjectService.java b/src/main/java/org/lab/service/ProjectService.java new file mode 100644 index 0000000..25a696d --- /dev/null +++ b/src/main/java/org/lab/service/ProjectService.java @@ -0,0 +1,46 @@ +package org.lab.service; + +import org.lab.model.*; +import org.lab.repository.ProjectRepository; + +import java.util.List; +import java.util.UUID; + +public class ProjectService { + + private final ProjectRepository repository; + private final RoleValidationService roleValidator; + + public ProjectService( + ProjectRepository repository, + RoleValidationService roleValidator + ) { + this.repository = repository; + this.roleValidator = roleValidator; + } + + public Project createProject(User creator, String name) { + Project project = new Project( + UUID.randomUUID(), + name, + List.of(new ProjectMembership(creator.id(), Role.MANAGER)), + List.of(), + List.of() + ); + repository.save(project); + return project; + } + + public Project addMember( + Project project, + User manager, + User user, + Role role + ) { + roleValidator.requireRole(project, manager, Role.MANAGER); + + Project updated = project.addMember(user.id(), role); + repository.save(updated); + return updated; + } +} diff --git a/src/main/java/org/lab/service/RoleValidationService.java b/src/main/java/org/lab/service/RoleValidationService.java new file mode 100644 index 0000000..e70b05d --- /dev/null +++ b/src/main/java/org/lab/service/RoleValidationService.java @@ -0,0 +1,23 @@ +package org.lab.service; + +import org.lab.model.Project; +import org.lab.model.Role; +import org.lab.model.User; + +import java.util.Arrays; + +public class RoleValidationService { + + public void requireRole(Project project, User user, Role... allowedRoles) { + Role actualRole = project.getUserRole(user.id()) + .orElseThrow(() -> + new RuntimeException("Пользователь не участвует в проекте")); + + boolean allowed = Arrays.stream(allowedRoles) + .anyMatch(r -> r == actualRole); + + if (!allowed) { + throw new RuntimeException(STR."Недостаточно прав. Роль пользователя: \{actualRole}"); + } + } +} diff --git a/src/main/java/org/lab/service/TicketService.java b/src/main/java/org/lab/service/TicketService.java new file mode 100644 index 0000000..f2a9699 --- /dev/null +++ b/src/main/java/org/lab/service/TicketService.java @@ -0,0 +1,82 @@ +package org.lab.service; + +import org.lab.model.*; +import org.lab.repository.TicketRepository; + +import java.util.UUID; + +public class TicketService { + + private final TicketRepository repository; + private final RoleValidationService roleValidator; + + public TicketService( + TicketRepository repository, + RoleValidationService roleValidator + ) { + this.repository = repository; + this.roleValidator = roleValidator; + } + + public Ticket createTicket( + User user, + Project project, + Milestone milestone, + String title, + String description + ) { + roleValidator.requireRole(project, user, Role.MANAGER, Role.TEAM_LEAD); + + Ticket ticket = new Ticket( + UUID.randomUUID(), + project.id(), + milestone.id(), + title, + description, + TicketStatus.NEW, + null + ); + + repository.save(ticket); + return ticket; + } + + public Ticket assignDeveloper( + User user, + Project project, + Ticket ticket, + User developer + ) { + roleValidator.requireRole(project, user, Role.MANAGER, Role.TEAM_LEAD); + + Ticket updated = ticket.assignDeveloper(developer.id()); + repository.save(updated); + return updated; + } + + public Ticket updateStatus( + User developer, + Project project, + Ticket ticket, + TicketStatus newStatus + ) { + roleValidator.requireRole(project, developer, Role.DEVELOPER); + + if (!developer.id().equals(ticket.assignedDeveloperId())) { + throw new RuntimeException("Тикет назначен другому разработчику"); + } + + Ticket updated = ticket.changeStatus(newStatus); + repository.save(updated); + return updated; + } + + public boolean isCompleted( + User user, + Project project, + Ticket ticket + ) { + roleValidator.requireRole(project, user, Role.MANAGER, Role.TEAM_LEAD); + return ticket.status() == TicketStatus.DONE; + } +} diff --git a/src/main/java/org/lab/service/UserService.java b/src/main/java/org/lab/service/UserService.java new file mode 100644 index 0000000..e995bb4 --- /dev/null +++ b/src/main/java/org/lab/service/UserService.java @@ -0,0 +1,51 @@ +package org.lab.service; + +import org.lab.model.*; +import org.lab.repository.*; + +import java.util.List; +import java.util.UUID; + +public class UserService { + + private final UserRepository userRepository; + private final ProjectRepository projectRepository; + private final TicketRepository ticketRepository; + private final BugReportRepository bugRepository; + + public UserService( + UserRepository userRepository, + ProjectRepository projectRepository, + TicketRepository ticketRepository, + BugReportRepository bugRepository + ) { + this.userRepository = userRepository; + this.projectRepository = projectRepository; + this.ticketRepository = ticketRepository; + this.bugRepository = bugRepository; + } + + public User register(String name) { + User user = new User(UUID.randomUUID(), name); + return userRepository.save(user); + } + + public List getUserProjects(User user) { + return projectRepository.findAll().stream() + .filter(p -> p.getUserRole(user.id()).isPresent()) + .toList(); + } + + public List getUserTickets(User user) { + return ticketRepository.findAll().stream() + .filter(t -> t.assignedDeveloperId() != null && user.id().equals(t.assignedDeveloperId())) + .toList(); + } + + public List getBugsToFix(User user) { + return bugRepository.findAll().stream() + .filter(b -> user.id().equals(b.assignedDeveloperId())) + .filter(b -> b.status() == BugStatus.NEW || b.status() == BugStatus.FIXED) + .toList(); + } +} diff --git a/src/main/java/org/lab/service/WorkflowService.java b/src/main/java/org/lab/service/WorkflowService.java new file mode 100644 index 0000000..3bf3e7a --- /dev/null +++ b/src/main/java/org/lab/service/WorkflowService.java @@ -0,0 +1,78 @@ +package org.lab.service; + +import org.lab.model.BugReport; +import org.lab.model.Milestone; +import org.lab.model.Project; +import org.lab.model.Ticket; +import org.lab.model.TicketStatus; +import org.lab.model.User; +import org.lab.workflow.Action; +import org.lab.workflow.AssignDeveloper; +import org.lab.workflow.CloseBug; +import org.lab.workflow.CompleteWork; +import org.lab.workflow.CreateTicket; +import org.lab.workflow.FixBug; +import org.lab.workflow.ReportBug; +import org.lab.workflow.StartWork; +import org.lab.workflow.TestBug; + +public class WorkflowService { + + private final TicketService ticketService; + private final BugReportService bugService; + + public WorkflowService( + TicketService ticketService, + BugReportService bugService + ) { + this.ticketService = ticketService; + this.bugService = bugService; + } + + @SuppressWarnings("unchecked") + public T apply( + User user, + Project project, + Object target, + Action action + ) { + return switch (action) { + + case CreateTicket ct -> (T) ticketService.createTicket( + user, project, (Milestone) target, + ct.title(), ct.description() + ); + + case AssignDeveloper ad -> (T) ticketService.assignDeveloper( + user, project, (Ticket) target, + new User(ad.developerId(), "tmp") + ); + + case StartWork _ -> (T) ticketService.updateStatus( + user, project, (Ticket) target, + TicketStatus.IN_PROGRESS + ); + + case CompleteWork _ -> (T) ticketService.updateStatus( + user, project, (Ticket) target, + TicketStatus.DONE + ); + + case ReportBug rb -> (T) bugService.createBug( + user, project, rb.description() + ); + + case FixBug _ -> (T) bugService.fixBug( + user, project, (BugReport) target + ); + + case TestBug _ -> (T) bugService.testBug( + user, project, (BugReport) target + ); + + case CloseBug _ -> (T) bugService.closeBug( + user, project, (BugReport) target + ); + }; + } +} diff --git a/src/main/java/org/lab/workflow/Action.java b/src/main/java/org/lab/workflow/Action.java new file mode 100644 index 0000000..f09eedf --- /dev/null +++ b/src/main/java/org/lab/workflow/Action.java @@ -0,0 +1,13 @@ +package org.lab.workflow; + +public sealed interface Action + permits CreateTicket, + AssignDeveloper, + StartWork, + CompleteWork, + ReportBug, + FixBug, + TestBug, + CloseBug { +} + diff --git a/src/main/java/org/lab/workflow/AssignDeveloper.java b/src/main/java/org/lab/workflow/AssignDeveloper.java new file mode 100644 index 0000000..1e7a1e1 --- /dev/null +++ b/src/main/java/org/lab/workflow/AssignDeveloper.java @@ -0,0 +1,9 @@ +package org.lab.workflow; + +import java.util.UUID; + +import org.lab.model.Ticket; + +public record AssignDeveloper( + UUID developerId +) implements Action {} diff --git a/src/main/java/org/lab/workflow/CloseBug.java b/src/main/java/org/lab/workflow/CloseBug.java new file mode 100644 index 0000000..c546488 --- /dev/null +++ b/src/main/java/org/lab/workflow/CloseBug.java @@ -0,0 +1,5 @@ +package org.lab.workflow; + +import org.lab.model.BugReport; + +public record CloseBug() implements Action {} diff --git a/src/main/java/org/lab/workflow/CompleteWork.java b/src/main/java/org/lab/workflow/CompleteWork.java new file mode 100644 index 0000000..dbc4d59 --- /dev/null +++ b/src/main/java/org/lab/workflow/CompleteWork.java @@ -0,0 +1,5 @@ +package org.lab.workflow; + +import org.lab.model.Ticket; + +public record CompleteWork() implements Action {} diff --git a/src/main/java/org/lab/workflow/CreateTicket.java b/src/main/java/org/lab/workflow/CreateTicket.java new file mode 100644 index 0000000..61c634d --- /dev/null +++ b/src/main/java/org/lab/workflow/CreateTicket.java @@ -0,0 +1,8 @@ +package org.lab.workflow; + +import org.lab.model.Ticket; + +public record CreateTicket( + String title, + String description +) implements Action {} diff --git a/src/main/java/org/lab/workflow/FixBug.java b/src/main/java/org/lab/workflow/FixBug.java new file mode 100644 index 0000000..dc2f8b0 --- /dev/null +++ b/src/main/java/org/lab/workflow/FixBug.java @@ -0,0 +1,5 @@ +package org.lab.workflow; + +import org.lab.model.BugReport; + +public record FixBug() implements Action {} diff --git a/src/main/java/org/lab/workflow/ReportBug.java b/src/main/java/org/lab/workflow/ReportBug.java new file mode 100644 index 0000000..67eb18c --- /dev/null +++ b/src/main/java/org/lab/workflow/ReportBug.java @@ -0,0 +1,7 @@ +package org.lab.workflow; + +import org.lab.model.BugReport; + +public record ReportBug( + String description +) implements Action {} diff --git a/src/main/java/org/lab/workflow/StartWork.java b/src/main/java/org/lab/workflow/StartWork.java new file mode 100644 index 0000000..8baaede --- /dev/null +++ b/src/main/java/org/lab/workflow/StartWork.java @@ -0,0 +1,5 @@ +package org.lab.workflow; + +import org.lab.model.Ticket; + +public record StartWork() implements Action {} diff --git a/src/main/java/org/lab/workflow/TestBug.java b/src/main/java/org/lab/workflow/TestBug.java new file mode 100644 index 0000000..a99748f --- /dev/null +++ b/src/main/java/org/lab/workflow/TestBug.java @@ -0,0 +1,5 @@ +package org.lab.workflow; + +import org.lab.model.BugReport; + +public record TestBug() implements Action {} diff --git a/src/test/java/org/lab/service/BugReportServiceTest.java b/src/test/java/org/lab/service/BugReportServiceTest.java new file mode 100644 index 0000000..5fd0b9a --- /dev/null +++ b/src/test/java/org/lab/service/BugReportServiceTest.java @@ -0,0 +1,41 @@ +package org.lab.service; + +import org.junit.jupiter.api.Test; +import org.lab.model.*; +import org.lab.repository.BugReportRepository; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class BugReportServiceTest { + + @Test + void bugLifecycleHappyPath() { + BugReportRepository repo = new BugReportRepository(); + RoleValidationService validator = new RoleValidationService(); + BugReportService service = new BugReportService(repo, validator); + + User dev = new User(UUID.randomUUID(), "Dev"); + User tester = new User(UUID.randomUUID(), "Tester"); + + Project project = new Project( + UUID.randomUUID(), + "Test", + java.util.List.of( + new ProjectMembership(dev.id(), Role.DEVELOPER), + new ProjectMembership(tester.id(), Role.TESTER) + ), + java.util.List.of(), + java.util.List.of() + ); + + BugReport bug = service.createBug(tester, project, "Bug"); + bug = service.fixBug(dev, project, bug); + bug = service.testBug(tester, project, bug); + bug = service.closeBug(tester, project, bug); + + assertEquals(BugStatus.CLOSED, bug.status()); + assertFalse(bug.isOpen()); + } +} diff --git a/src/test/java/org/lab/service/MilestoneServiceTest.java b/src/test/java/org/lab/service/MilestoneServiceTest.java new file mode 100644 index 0000000..905d511 --- /dev/null +++ b/src/test/java/org/lab/service/MilestoneServiceTest.java @@ -0,0 +1,46 @@ +package org.lab.service; + +import org.junit.jupiter.api.Test; +import org.lab.model.*; +import org.lab.repository.*; + +import java.time.LocalDate; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class MilestoneServiceTest { + + @Test + void onlyOneActiveMilestoneAllowed() { + MilestoneRepository milestoneRepo = new MilestoneRepository(); + TicketRepository ticketRepo = new TicketRepository(); + RoleValidationService validator = new RoleValidationService(); + + MilestoneService service = + new MilestoneService(milestoneRepo, ticketRepo, validator); + + User manager = new User(UUID.randomUUID(), "Manager"); + + Project project = new Project( + UUID.randomUUID(), + "Test", + java.util.List.of(new ProjectMembership(manager.id(), Role.MANAGER)), + java.util.List.of(), + java.util.List.of() + ); + + service.createMilestone( + manager, project, "M1", + LocalDate.now(), LocalDate.now().plusDays(5) + ); + + assertThrows( + IllegalStateException.class, + () -> service.createMilestone( + manager, project, "M2", + LocalDate.now(), LocalDate.now().plusDays(5) + ) + ); + } +} diff --git a/src/test/java/org/lab/service/ProjectServiceTest.java b/src/test/java/org/lab/service/ProjectServiceTest.java new file mode 100644 index 0000000..cbe9856 --- /dev/null +++ b/src/test/java/org/lab/service/ProjectServiceTest.java @@ -0,0 +1,48 @@ +package org.lab.service; + +import org.junit.jupiter.api.Test; +import org.lab.model.*; +import org.lab.repository.ProjectRepository; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class ProjectServiceTest { + + @Test + void managerCanAddMembers() { + ProjectRepository projectRepo = new ProjectRepository(); + RoleValidationService validator = new RoleValidationService(); + ProjectService projectService = new ProjectService(projectRepo, validator); + + User manager = new User(UUID.randomUUID(), "Manager"); + User dev = new User(UUID.randomUUID(), "Dev"); + + Project project = projectService.createProject(manager, "Test"); + + project = projectService.addMember(project, manager, dev, Role.DEVELOPER); + + assertEquals( + Role.DEVELOPER, + project.getUserRole(dev.id()).orElseThrow() + ); + } + + @Test + void nonManagerCannotAddMembers() { + ProjectRepository projectRepo = new ProjectRepository(); + RoleValidationService validator = new RoleValidationService(); + ProjectService projectService = new ProjectService(projectRepo, validator); + + User manager = new User(UUID.randomUUID(), "Manager"); + User outsider = new User(UUID.randomUUID(), "Outsider"); + + Project project = projectService.createProject(manager, "Test"); + + assertThrows( + RuntimeException.class, + () -> projectService.addMember(project, outsider, manager, Role.DEVELOPER) + ); + } +} diff --git a/src/test/java/org/lab/service/TicketServiceTest.java b/src/test/java/org/lab/service/TicketServiceTest.java new file mode 100644 index 0000000..d31c3cf --- /dev/null +++ b/src/test/java/org/lab/service/TicketServiceTest.java @@ -0,0 +1,52 @@ +package org.lab.service; + +import org.junit.jupiter.api.Test; +import org.lab.model.*; +import org.lab.repository.TicketRepository; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class TicketServiceTest { + + @Test + void developerCanCompleteAssignedTicket() { + TicketRepository repo = new TicketRepository(); + RoleValidationService validator = new RoleValidationService(); + TicketService service = new TicketService(repo, validator); + + User manager = new User(UUID.randomUUID(), "Manager"); + User dev = new User(UUID.randomUUID(), "Dev"); + + Project project = new Project( + UUID.randomUUID(), + "Test", + java.util.List.of( + new ProjectMembership(manager.id(), Role.MANAGER), + new ProjectMembership(dev.id(), Role.DEVELOPER) + ), + java.util.List.of(), + java.util.List.of() + ); + + Milestone milestone = new Milestone( + UUID.randomUUID(), + project.id(), + "M1", + java.time.LocalDate.now(), + java.time.LocalDate.now().plusDays(7), + MilestoneStatus.ACTIVE, + java.util.List.of() + ); + + Ticket ticket = service.createTicket( + manager, project, milestone, "Task", "Desc" + ); + + ticket = service.assignDeveloper(manager, project, ticket, dev); + ticket = service.updateStatus(dev, project, ticket, TicketStatus.DONE); + + assertTrue(ticket.isCompleted()); + } +} From ce57acf919598373f8c1d0f49a4e46b5213cd110 Mon Sep 17 00:00:00 2001 From: Vladislav Bandurin Date: Wed, 14 Jan 2026 19:31:53 +0300 Subject: [PATCH 3/4] fix gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b63da45..c395192 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ build/ !**/src/test/**/build/ ### IntelliJ IDEA ### +.idea .idea/modules.xml .idea/jarRepositories.xml .idea/compiler.xml @@ -38,5 +39,7 @@ bin/ ### VS Code ### .vscode/ +.java-version + ### Mac OS ### .DS_Store \ No newline at end of file From 2fd9871075773467972310a193aff8ca2fb5afef Mon Sep 17 00:00:00 2001 From: Vladislav Bandurin Date: Wed, 14 Jan 2026 19:32:28 +0300 Subject: [PATCH 4/4] fix --- .idea/.gitignore | 3 --- .idea/.name | 1 - .idea/gradle.xml | 15 --------------- .idea/inspectionProfiles/Project_Default.xml | 6 ------ .idea/misc.xml | 5 ----- .idea/vcs.xml | 6 ------ .java-version | 1 - 7 files changed, 37 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/.name delete mode 100644 .idea/gradle.xml delete mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/vcs.xml delete mode 100644 .java-version diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 0194542..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -features \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index f9163b4..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index ab89d8f..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 313aec6..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.java-version b/.java-version deleted file mode 100644 index aabe6ec..0000000 --- a/.java-version +++ /dev/null @@ -1 +0,0 @@ -21