Refactor org backend and remove obsolete code
All checks were successful
Build Org Backend / build (push) Successful in 15s
All checks were successful
Build Org Backend / build (push) Successful in 15s
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,6 +2,8 @@
|
|||||||
target/
|
target/
|
||||||
application.properties
|
application.properties
|
||||||
application-*.properties
|
application-*.properties
|
||||||
|
!src/main/resources/application-dev.properties
|
||||||
|
!src/main/resources/application-prod.properties
|
||||||
.settings/
|
.settings/
|
||||||
.project
|
.project
|
||||||
.classpath
|
.classpath
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ Create that file once on the server with the production values required by the
|
|||||||
app. The Gitea deploy step verifies that it exists, but it does not create or
|
app. The Gitea deploy step verifies that it exists, but it does not create or
|
||||||
overwrite it.
|
overwrite it.
|
||||||
|
|
||||||
|
Datasource credentials and `JWT_SECRET` are required environment values; they
|
||||||
|
are intentionally not stored in the Spring profile files. Start from
|
||||||
|
`misc/org-backend.env.example` and keep the populated file outside the
|
||||||
|
repository. Rotate any credential that was previously committed before using
|
||||||
|
this version in production.
|
||||||
|
|
||||||
The live systemd unit at `/etc/systemd/system/org-backend.service` is managed
|
The live systemd unit at `/etc/systemd/system/org-backend.service` is managed
|
||||||
as a symlink to `misc/org-backend.service`, so the project copy is the source of
|
as a symlink to `misc/org-backend.service`, so the project copy is the source of
|
||||||
truth.
|
truth.
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ GUACAMOLE_RUN_DIR=/home/zaine
|
|||||||
GUACAMOLE_CONTAINER_NAME=guacamole
|
GUACAMOLE_CONTAINER_NAME=guacamole
|
||||||
GUACAMOLE_START_LOG=/home/zaine/logs/guacamole-start.log
|
GUACAMOLE_START_LOG=/home/zaine/logs/guacamole-start.log
|
||||||
GUACAMOLE_STOP_LOG=/home/zaine/logs/guacamole-stop.log
|
GUACAMOLE_STOP_LOG=/home/zaine/logs/guacamole-stop.log
|
||||||
NOSTALGIA_RUN_DIR=/home/zaine/master-folder/projects/_personal/nostalgia
|
|
||||||
NOSTALGIA_RUN_LOG=/home/zaine/logs/nostalgia-prod.log
|
NOSTALGIA_RUN_LOG=/home/zaine/logs/nostalgia-prod.log
|
||||||
|
|
||||||
PLAY_RPG_SAVE_DIR=/home/zaine/master-folder/org-platform/org_backend/data/rpg-saves
|
PLAY_RPG_SAVE_DIR=/home/zaine/master-folder/org-platform/org_backend/data/rpg-saves
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package org.zaine.app.calibre.adapter.in.web;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.zaine.app.calibre.application.port.in.BrowseCalibreBooksUseCase;
|
||||||
|
import org.zaine.app.calibre.domain.CalibreBook;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/calibre")
|
||||||
|
@Tag(name = "Calibre", description = "Calibre catalogue")
|
||||||
|
public class CalibreController {
|
||||||
|
private final BrowseCalibreBooksUseCase books;
|
||||||
|
|
||||||
|
public CalibreController(BrowseCalibreBooksUseCase books) {
|
||||||
|
this.books = books;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/books")
|
||||||
|
public List<BookResponse> getBooks() {
|
||||||
|
return books.getBooks().stream().map(BookResponse::from).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record BookResponse(long id, String title, String authors) {
|
||||||
|
static BookResponse from(CalibreBook book) {
|
||||||
|
return new BookResponse(book.id(), book.title(), book.authors());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package org.zaine.app.calibre.adapter.out.sqlite;
|
||||||
|
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.zaine.app.calibre.application.port.out.CalibreCatalogPort;
|
||||||
|
import org.zaine.app.calibre.domain.CalibreBook;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class SqliteCalibreCatalog implements CalibreCatalogPort {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SqliteCalibreCatalog.class);
|
||||||
|
private static final String QUERY = """
|
||||||
|
SELECT b.id AS book_id, b.title AS title, GROUP_CONCAT(a.name, ', ') AS authors
|
||||||
|
FROM books b
|
||||||
|
LEFT JOIN books_authors_link bal ON b.id = bal.book
|
||||||
|
LEFT JOIN authors a ON bal.author = a.id
|
||||||
|
GROUP BY b.id
|
||||||
|
ORDER BY b.title COLLATE NOCASE
|
||||||
|
""";
|
||||||
|
|
||||||
|
private final String databasePath;
|
||||||
|
|
||||||
|
SqliteCalibreCatalog(@Value("${calibre.db.path}") String databasePath) {
|
||||||
|
this.databasePath = databasePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CalibreBook> findAll() {
|
||||||
|
List<CalibreBook> books = new ArrayList<>();
|
||||||
|
try (var connection = DriverManager.getConnection("jdbc:sqlite:" + databasePath);
|
||||||
|
var statement = connection.createStatement();
|
||||||
|
var results = statement.executeQuery(QUERY)) {
|
||||||
|
while (results.next()) {
|
||||||
|
books.add(new CalibreBook(results.getLong("book_id"), results.getString("title"),
|
||||||
|
results.getString("authors")));
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("Calibre catalogue is unavailable at {}: {}", databasePath, ex.getMessage());
|
||||||
|
}
|
||||||
|
return books;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.zaine.app.calibre.application.port.in;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.zaine.app.calibre.domain.CalibreBook;
|
||||||
|
|
||||||
|
public interface BrowseCalibreBooksUseCase {
|
||||||
|
List<CalibreBook> getBooks();
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.zaine.app.calibre.application.port.out;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.zaine.app.calibre.domain.CalibreBook;
|
||||||
|
|
||||||
|
public interface CalibreCatalogPort {
|
||||||
|
List<CalibreBook> findAll();
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package org.zaine.app.calibre.application.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.zaine.app.calibre.application.port.in.BrowseCalibreBooksUseCase;
|
||||||
|
import org.zaine.app.calibre.application.port.out.CalibreCatalogPort;
|
||||||
|
import org.zaine.app.calibre.domain.CalibreBook;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CalibreApplicationService implements BrowseCalibreBooksUseCase {
|
||||||
|
private final CalibreCatalogPort catalog;
|
||||||
|
|
||||||
|
public CalibreApplicationService(CalibreCatalogPort catalog) {
|
||||||
|
this.catalog = catalog;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CalibreBook> getBooks() {
|
||||||
|
return catalog.findAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package org.zaine.app.calibre.domain;
|
||||||
|
|
||||||
|
public record CalibreBook(long id, String title, String authors) {
|
||||||
|
public CalibreBook {
|
||||||
|
authors = authors == null ? "" : authors;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package org.zaine.app.common.application;
|
||||||
|
|
||||||
|
public class ApplicationException extends RuntimeException {
|
||||||
|
public enum Kind { BAD_REQUEST, NOT_FOUND, UNAUTHORIZED, PAYLOAD_TOO_LARGE, FAILURE }
|
||||||
|
|
||||||
|
private final Kind kind;
|
||||||
|
|
||||||
|
private ApplicationException(Kind kind, String message) {
|
||||||
|
super(message == null || message.isBlank() ? "Operation failed" : message);
|
||||||
|
this.kind = kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Kind kind() {
|
||||||
|
return kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ApplicationException badRequest(String message) {
|
||||||
|
return new ApplicationException(Kind.BAD_REQUEST, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ApplicationException notFound(String message) {
|
||||||
|
return new ApplicationException(Kind.NOT_FOUND, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ApplicationException unauthorized(String message) {
|
||||||
|
return new ApplicationException(Kind.UNAUTHORIZED, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ApplicationException payloadTooLarge(String message) {
|
||||||
|
return new ApplicationException(Kind.PAYLOAD_TOO_LARGE, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ApplicationException failure(String message) {
|
||||||
|
return new ApplicationException(Kind.FAILURE, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/main/java/org/zaine/app/config/BuildProperties.java
Normal file
64
src/main/java/org/zaine/app/config/BuildProperties.java
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package org.zaine.app.config;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class BuildProperties {
|
||||||
|
private final String webDirectory;
|
||||||
|
private final String webLog;
|
||||||
|
private final String emacsDirectory;
|
||||||
|
private final String emacsLog;
|
||||||
|
private final String combinedLog;
|
||||||
|
private final String resourceDirectory;
|
||||||
|
private final String resourceLog;
|
||||||
|
private final String resourceCommand;
|
||||||
|
private final String guacamoleDirectory;
|
||||||
|
private final String guacamoleContainer;
|
||||||
|
private final String guacamoleStartLog;
|
||||||
|
private final String guacamoleStopLog;
|
||||||
|
private final String nostalgiaLog;
|
||||||
|
|
||||||
|
public BuildProperties(
|
||||||
|
@Value("${zone.build.dir}") String webDirectory,
|
||||||
|
@Value("${zone.build.log}") String webLog,
|
||||||
|
@Value("${emacs.run.dir}") String emacsDirectory,
|
||||||
|
@Value("${emacs.run.log}") String emacsLog,
|
||||||
|
@Value("${combined.run.log}") String combinedLog,
|
||||||
|
@Value("${adventure.resources.free.dir:/home/zaine}") String resourceDirectory,
|
||||||
|
@Value("${adventure.resources.free.log:/home/zaine/logs/adventure-resources.log}") String resourceLog,
|
||||||
|
@Value("${adventure.resources.free.command:sudo -n sh -c 'sync; echo 3 > /proc/sys/vm/drop_caches' && /usr/bin/docker system prune -af && /usr/bin/docker builder prune -af}") String resourceCommand,
|
||||||
|
@Value("${guacamole.run.dir:/home/zaine}") String guacamoleDirectory,
|
||||||
|
@Value("${guacamole.container.name:guacamole}") String guacamoleContainer,
|
||||||
|
@Value("${guacamole.start.log:/home/zaine/logs/guacamole-start.log}") String guacamoleStartLog,
|
||||||
|
@Value("${guacamole.stop.log:/home/zaine/logs/guacamole-stop.log}") String guacamoleStopLog,
|
||||||
|
@Value("${nostalgia.run.log:/home/zaine/logs/nostalgia-prod.log}") String nostalgiaLog) {
|
||||||
|
this.webDirectory = webDirectory;
|
||||||
|
this.webLog = webLog;
|
||||||
|
this.emacsDirectory = emacsDirectory;
|
||||||
|
this.emacsLog = emacsLog;
|
||||||
|
this.combinedLog = combinedLog;
|
||||||
|
this.resourceDirectory = resourceDirectory;
|
||||||
|
this.resourceLog = resourceLog;
|
||||||
|
this.resourceCommand = resourceCommand;
|
||||||
|
this.guacamoleDirectory = guacamoleDirectory;
|
||||||
|
this.guacamoleContainer = guacamoleContainer;
|
||||||
|
this.guacamoleStartLog = guacamoleStartLog;
|
||||||
|
this.guacamoleStopLog = guacamoleStopLog;
|
||||||
|
this.nostalgiaLog = nostalgiaLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String webDirectory() { return webDirectory; }
|
||||||
|
public String webLog() { return webLog; }
|
||||||
|
public String emacsDirectory() { return emacsDirectory; }
|
||||||
|
public String emacsLog() { return emacsLog; }
|
||||||
|
public String combinedLog() { return combinedLog; }
|
||||||
|
public String resourceDirectory() { return resourceDirectory; }
|
||||||
|
public String resourceLog() { return resourceLog; }
|
||||||
|
public String resourceCommand() { return resourceCommand; }
|
||||||
|
public String guacamoleDirectory() { return guacamoleDirectory; }
|
||||||
|
public String guacamoleContainer() { return guacamoleContainer; }
|
||||||
|
public String guacamoleStartLog() { return guacamoleStartLog; }
|
||||||
|
public String guacamoleStopLog() { return guacamoleStopLog; }
|
||||||
|
public String nostalgiaLog() { return nostalgiaLog; }
|
||||||
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package org.zaine.app.controller;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import org.zaine.app.dto.CalibreBookDTO;
|
|
||||||
import org.zaine.app.service.CalibreService;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/calibre")
|
|
||||||
@Tag(name = "Calibre", description = "Calibre API")
|
|
||||||
public class CalibreController {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private CalibreService calibreService;
|
|
||||||
|
|
||||||
@Operation(summary = "Get all books", description = "Fetches all books from the Calibre database")
|
|
||||||
@ApiResponses(value = {
|
|
||||||
@ApiResponse(responseCode = "200", description = "Books retrieved successfully"),
|
|
||||||
@ApiResponse(responseCode = "404", description = "No books found"),
|
|
||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
|
||||||
})
|
|
||||||
@GetMapping("/books")
|
|
||||||
public List<CalibreBookDTO> getBooks() {
|
|
||||||
return calibreService.getAllBooks();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package org.zaine.app.controller;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import org.zaine.app.dto.EveningCheckinRequest;
|
|
||||||
import org.zaine.app.dto.MorningCheckinRequest;
|
|
||||||
import org.zaine.app.dto.TodayStatusDTO;
|
|
||||||
import org.zaine.app.service.CheckinService;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/checkin")
|
|
||||||
@Tag(name = "Check-in", description = "Daily morning and evening check-ins")
|
|
||||||
public class CheckinController {
|
|
||||||
|
|
||||||
private final CheckinService checkinService;
|
|
||||||
|
|
||||||
public CheckinController(CheckinService checkinService) {
|
|
||||||
this.checkinService = checkinService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Get today's check-in status")
|
|
||||||
@GetMapping("/today-status")
|
|
||||||
public TodayStatusDTO getTodayStatus() {
|
|
||||||
return checkinService.getTodayStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Get check-in history")
|
|
||||||
@GetMapping("/history")
|
|
||||||
public List<TodayStatusDTO> getHistory(
|
|
||||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
|
|
||||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
|
|
||||||
return checkinService.getHistory(from, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Submit morning check-in")
|
|
||||||
@PostMapping("/morning")
|
|
||||||
public ResponseEntity<TodayStatusDTO> submitMorning(@RequestBody MorningCheckinRequest request) {
|
|
||||||
return ResponseEntity.ok(checkinService.saveMorning(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Submit evening check-in")
|
|
||||||
@PostMapping("/evening")
|
|
||||||
public ResponseEntity<TodayStatusDTO> submitEvening(@RequestBody EveningCheckinRequest request) {
|
|
||||||
return ResponseEntity.ok(checkinService.saveEvening(request));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
package org.zaine.app.controller;
|
package org.zaine.app.controller;
|
||||||
|
|
||||||
import java.lang.System.Logger;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -24,10 +23,11 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
@Tag(name = "Comments", description = "Endpoints for managing comments")
|
@Tag(name = "Comments", description = "Endpoints for managing comments")
|
||||||
public class CommentsController {
|
public class CommentsController {
|
||||||
|
|
||||||
private static final Logger logger = System.getLogger(CommentsController.class.getName());
|
private final CommentsService commentsService;
|
||||||
|
|
||||||
@Autowired
|
public CommentsController(CommentsService commentsService) {
|
||||||
private CommentsService commentsService;
|
this.commentsService = commentsService;
|
||||||
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get comments by page slug",
|
summary = "Get comments by page slug",
|
||||||
@@ -39,9 +39,8 @@ public class CommentsController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@GetMapping("/{page_slug}")
|
@GetMapping("/{page_slug}")
|
||||||
public List<Comments> getAllCommentsBySlug(@PathVariable String page_slug) {
|
public List<CommentResponse> getAllCommentsBySlug(@PathVariable String page_slug) {
|
||||||
List<Comments> allComments = commentsService.getAllCommentsBySlug(page_slug);
|
return commentsService.getAllCommentsBySlug(page_slug).stream().map(CommentResponse::from).toList();
|
||||||
return allComments;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -54,8 +53,8 @@ public class CommentsController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@GetMapping("/item/{id}")
|
@GetMapping("/item/{id}")
|
||||||
public Comments getCommentById(@PathVariable Integer id) {
|
public CommentResponse getCommentById(@PathVariable Integer id) {
|
||||||
return commentsService.getCommentById(id);
|
return CommentResponse.from(commentsService.getCommentById(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -68,8 +67,8 @@ public class CommentsController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@GetMapping("")
|
@GetMapping("")
|
||||||
public List<Comments> getAllComments() {
|
public List<CommentResponse> getAllComments() {
|
||||||
return commentsService.getAllComments();
|
return commentsService.getAllComments().stream().map(CommentResponse::from).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -87,22 +86,7 @@ public class CommentsController {
|
|||||||
)
|
)
|
||||||
public void addComment(@RequestBody CreateCommentDTO dto) {
|
public void addComment(@RequestBody CreateCommentDTO dto) {
|
||||||
|
|
||||||
if (dto.getContent() == null || dto.getContent().trim().isEmpty()) {
|
commentsService.addComment(dto.getPageSlug(), dto.getAuthor(), dto.getContent(), dto.getParentId());
|
||||||
logger.log(System.Logger.Level.WARNING,
|
|
||||||
"Attempted to add empty comment. Operation aborted.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Comments newComment = new Comments();
|
|
||||||
newComment.setPageSlug(dto.getPageSlug());
|
|
||||||
newComment.setAuthor(dto.getAuthor());
|
|
||||||
newComment.setContent(dto.getContent());
|
|
||||||
newComment.setParentId(dto.getParentId());
|
|
||||||
newComment.setCreatedAt(java.time.Instant.now());
|
|
||||||
|
|
||||||
logger.log(System.Logger.Level.INFO, () -> "Adding comment to pageSlug: " + dto.getPageSlug());
|
|
||||||
|
|
||||||
commentsService.addComment(newComment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -115,7 +99,21 @@ public class CommentsController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@GetMapping("/thread/{comment_id}")
|
@GetMapping("/thread/{comment_id}")
|
||||||
public List<Comments> getCommentThread(@PathVariable Integer comment_id) {
|
public List<CommentResponse> getCommentThread(@PathVariable Integer comment_id) {
|
||||||
return commentsService.getCommentThread(comment_id);
|
return commentsService.getCommentThread(comment_id).stream().map(CommentResponse::from).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CommentResponse(
|
||||||
|
Integer id,
|
||||||
|
String pageSlug,
|
||||||
|
String author,
|
||||||
|
String content,
|
||||||
|
@JsonProperty("created_at") java.time.Instant createdAt,
|
||||||
|
@JsonProperty("parent_id") Integer parentId) {
|
||||||
|
static CommentResponse from(Comments comment) {
|
||||||
|
if (comment == null) return null;
|
||||||
|
return new CommentResponse(comment.getId(), comment.getPageSlug(), comment.getAuthor(),
|
||||||
|
comment.getContent(), comment.getCreatedAt(), comment.getParentId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package org.zaine.app.controller;
|
package org.zaine.app.controller;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -23,8 +22,11 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
@Tag(name = "Competencies", description = "Operations about competencies")
|
@Tag(name = "Competencies", description = "Operations about competencies")
|
||||||
public class CompetenciesController {
|
public class CompetenciesController {
|
||||||
|
|
||||||
@Autowired
|
private final CompetenciesService competenciesService;
|
||||||
private CompetenciesService competenciesService;
|
|
||||||
|
public CompetenciesController(CompetenciesService competenciesService) {
|
||||||
|
this.competenciesService = competenciesService;
|
||||||
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get all competencies",
|
summary = "Get all competencies",
|
||||||
@@ -36,12 +38,12 @@ public class CompetenciesController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@GetMapping("/items")
|
@GetMapping("/items")
|
||||||
public List<Competencies> getAllCompetencies(
|
public List<CompetencyResponse> getAllCompetencies(
|
||||||
@RequestParam(value = "group", required = false) String group) {
|
@RequestParam(value = "group", required = false) String group) {
|
||||||
if (group != null && !group.isBlank()) {
|
if (group != null && !group.isBlank()) {
|
||||||
return competenciesService.getCompetenciesByGroup(group);
|
return competenciesService.getCompetenciesByGroup(group).stream().map(CompetencyResponse::from).toList();
|
||||||
}
|
}
|
||||||
return competenciesService.getAllCompetencies();
|
return competenciesService.getAllCompetencies().stream().map(CompetencyResponse::from).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -54,8 +56,8 @@ public class CompetenciesController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@GetMapping("/item/{id}")
|
@GetMapping("/item/{id}")
|
||||||
public Competencies getCompetencyById(@PathVariable Integer id) {
|
public CompetencyResponse getCompetencyById(@PathVariable Integer id) {
|
||||||
return competenciesService.getCompetencyById(id);
|
return CompetencyResponse.from(competenciesService.getCompetencyById(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -77,4 +79,12 @@ public class CompetenciesController {
|
|||||||
) {
|
) {
|
||||||
competenciesService.updateCompetencyState(id, request.getState());
|
competenciesService.updateCompetencyState(id, request.getState());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record CompetencyResponse(Integer id, String title, String state, java.time.Instant createdAt, String group) {
|
||||||
|
static CompetencyResponse from(Competencies competency) {
|
||||||
|
if (competency == null) return null;
|
||||||
|
return new CompetencyResponse(competency.getId(), competency.getTitle(), competency.getState(),
|
||||||
|
competency.getCreatedAt(), competency.getGroup());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
package org.zaine.app.controller;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import org.zaine.app.dto.CreateNoteDTO;
|
|
||||||
import org.zaine.app.model.Notes;
|
|
||||||
import org.zaine.app.service.NotesService;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api")
|
|
||||||
@Tag(name = "Notes", description = "Operations related to notes")
|
|
||||||
public class NotesController {
|
|
||||||
@Autowired
|
|
||||||
private NotesService notesService;
|
|
||||||
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Get all notes",
|
|
||||||
description = "Returns a list of all notes"
|
|
||||||
)
|
|
||||||
@ApiResponses(value = {
|
|
||||||
@ApiResponse(responseCode = "200", description = "Notes retrieved successfully"),
|
|
||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
|
||||||
})
|
|
||||||
@GetMapping("/notes")
|
|
||||||
public List<Notes> getAllNotes() {
|
|
||||||
return notesService.getAllNotes();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Create a note object",
|
|
||||||
description = "Creates a new note"
|
|
||||||
)
|
|
||||||
@ApiResponses(value = {
|
|
||||||
@ApiResponse(responseCode = "200", description = "Note created successfully"),
|
|
||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
|
||||||
})
|
|
||||||
@PostMapping(
|
|
||||||
value = "/notes",
|
|
||||||
consumes = "application/json"
|
|
||||||
)
|
|
||||||
public Notes createNote(@RequestBody CreateNoteDTO dto) {
|
|
||||||
|
|
||||||
Notes note = new Notes();
|
|
||||||
note.setAuthorName(dto.getAuthorName());
|
|
||||||
note.setContent(dto.getContent());
|
|
||||||
|
|
||||||
return notesService.createNote(note);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
package org.zaine.app.controller;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.CalendarDayDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.CreateSymptomRequest;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.DailyLogDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.DailyLogRequest;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.DashboardDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.InsightsDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.FastingLogDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.FastingLogRequest;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.PeriodCycleDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.PeriodCycleRequest;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.PeriodEntryRequest;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.PredictionDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.ReflectionDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.ReflectionRequest;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SettingsDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SettingsRequest;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SupportMessageDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SupportMessageRequest;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SymptomDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.WellbeingDTO;
|
|
||||||
import org.zaine.app.service.PeriodCompanionService;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/period-companion")
|
|
||||||
@Tag(name = "Period Companion", description = "Private menstrual cycle tracking for shared visibility")
|
|
||||||
public class PeriodCompanionController {
|
|
||||||
private final PeriodCompanionService periodCompanionService;
|
|
||||||
|
|
||||||
public PeriodCompanionController(PeriodCompanionService periodCompanionService) {
|
|
||||||
this.periodCompanionService = periodCompanionService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Get period companion dashboard")
|
|
||||||
@GetMapping("/dashboard")
|
|
||||||
public DashboardDTO getDashboard() {
|
|
||||||
return periodCompanionService.getDashboard();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Get calendar days for a date range")
|
|
||||||
@GetMapping("/calendar")
|
|
||||||
public List<CalendarDayDTO> getCalendar(
|
|
||||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
|
|
||||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
|
|
||||||
return periodCompanionService.getCalendar(from, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Get daily logs for a date range")
|
|
||||||
@GetMapping("/daily-logs")
|
|
||||||
public List<DailyLogDTO> getDailyLogs(
|
|
||||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
|
|
||||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
|
|
||||||
return periodCompanionService.getDailyLogs(from, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Get a daily log by date")
|
|
||||||
@GetMapping("/daily-logs/{date}")
|
|
||||||
public DailyLogDTO getDailyLog(@PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
|
|
||||||
return periodCompanionService.getDailyLog(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Create or update a daily log")
|
|
||||||
@PostMapping("/daily-logs")
|
|
||||||
public ResponseEntity<DailyLogDTO> saveDailyLog(@RequestBody DailyLogRequest request) {
|
|
||||||
return ResponseEntity.ok(periodCompanionService.saveDailyLog(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Get symptoms")
|
|
||||||
@GetMapping("/symptoms")
|
|
||||||
public List<SymptomDTO> getSymptoms() {
|
|
||||||
return periodCompanionService.getSymptoms();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Create a symptom")
|
|
||||||
@PostMapping("/symptoms")
|
|
||||||
public ResponseEntity<SymptomDTO> createSymptom(@RequestBody CreateSymptomRequest request) {
|
|
||||||
return ResponseEntity.ok(periodCompanionService.createSymptom(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Get tracked cycles")
|
|
||||||
@GetMapping("/cycles")
|
|
||||||
public List<PeriodCycleDTO> getCycles() {
|
|
||||||
return periodCompanionService.getCycles();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Create or update a cycle")
|
|
||||||
@PostMapping("/cycles")
|
|
||||||
public ResponseEntity<PeriodCycleDTO> saveCycle(@RequestBody PeriodCycleRequest request) {
|
|
||||||
return ResponseEntity.ok(periodCompanionService.saveCycle(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Create or update a period entry and matching daily flow logs")
|
|
||||||
@PostMapping("/period-entries")
|
|
||||||
public ResponseEntity<PeriodCycleDTO> savePeriodEntry(@RequestBody PeriodEntryRequest request) {
|
|
||||||
return ResponseEntity.ok(periodCompanionService.savePeriodEntry(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Generate and store a prediction snapshot")
|
|
||||||
@PostMapping("/predictions/generate")
|
|
||||||
public ResponseEntity<PredictionDTO> generatePrediction() {
|
|
||||||
return ResponseEntity.ok(periodCompanionService.generatePrediction());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Get period companion insights")
|
|
||||||
@GetMapping("/insights")
|
|
||||||
public InsightsDTO getInsights() {
|
|
||||||
return periodCompanionService.getInsights();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Get cycle-aware wellbeing guidance")
|
|
||||||
@GetMapping("/wellbeing")
|
|
||||||
public WellbeingDTO getWellbeing() {
|
|
||||||
return periodCompanionService.getWellbeing();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Get fasting make-up logs")
|
|
||||||
@GetMapping("/fasting-logs")
|
|
||||||
public List<FastingLogDTO> getFastingLogs() {
|
|
||||||
return periodCompanionService.getFastingLogs();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Create or update a fasting make-up log")
|
|
||||||
@PostMapping("/fasting-logs")
|
|
||||||
public ResponseEntity<FastingLogDTO> saveFastingLog(@RequestBody FastingLogRequest request) {
|
|
||||||
return ResponseEntity.ok(periodCompanionService.saveFastingLog(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Create or update a daily reflection")
|
|
||||||
@PostMapping("/reflections")
|
|
||||||
public ResponseEntity<ReflectionDTO> saveReflection(@RequestBody ReflectionRequest request) {
|
|
||||||
return ResponseEntity.ok(periodCompanionService.saveReflection(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Get period companion settings")
|
|
||||||
@GetMapping("/settings")
|
|
||||||
public SettingsDTO getSettings() {
|
|
||||||
return periodCompanionService.getSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Update period companion settings")
|
|
||||||
@PostMapping("/settings")
|
|
||||||
public ResponseEntity<SettingsDTO> updateSettings(@RequestBody SettingsRequest request) {
|
|
||||||
return ResponseEntity.ok(periodCompanionService.updateSettings(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Get recent support messages")
|
|
||||||
@GetMapping("/support-messages")
|
|
||||||
public List<SupportMessageDTO> getSupportMessages() {
|
|
||||||
return periodCompanionService.getSupportMessages();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "Create a support message")
|
|
||||||
@PostMapping("/support-messages")
|
|
||||||
public ResponseEntity<SupportMessageDTO> createSupportMessage(@RequestBody SupportMessageRequest request) {
|
|
||||||
return ResponseEntity.ok(periodCompanionService.createSupportMessage(request));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package org.zaine.app.controller;
|
package org.zaine.app.controller;
|
||||||
|
|
||||||
import java.lang.System.Logger;
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
@@ -29,22 +29,24 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
@Tag(name = "Wird", description = "Wird API")
|
@Tag(name = "Wird", description = "Wird API")
|
||||||
public class WirdController {
|
public class WirdController {
|
||||||
|
|
||||||
private static final Logger logger = System.getLogger(WirdController.class.getName());
|
private static final Logger log = LoggerFactory.getLogger(WirdController.class);
|
||||||
|
|
||||||
@Autowired
|
private final WirdService wirdService;
|
||||||
private WirdService wirdService;
|
private final MotalahService motalahService;
|
||||||
|
|
||||||
@Autowired
|
public WirdController(WirdService wirdService, MotalahService motalahService) {
|
||||||
private MotalahService motalahService;
|
this.wirdService = wirdService;
|
||||||
|
this.motalahService = motalahService;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/wird/entries
|
* GET /api/wird/entries
|
||||||
* Returns all entries, newest first. Used by history table and today cards.
|
* Returns all entries, newest first. Used by history table and today cards.
|
||||||
*/
|
*/
|
||||||
@GetMapping("/entries")
|
@GetMapping("/entries")
|
||||||
public List<WirdEntry> getAllEntries() {
|
public List<WirdEntryResponse> getAllEntries() {
|
||||||
logger.log(Logger.Level.INFO, "Fetching all wird entries");
|
log.debug("Fetching all wird entries");
|
||||||
return wirdService.getAllEntries();
|
return toWirdResponses(wirdService.getAllEntries());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,8 +54,8 @@ public class WirdController {
|
|||||||
* Convenience endpoint for today's entries only.
|
* Convenience endpoint for today's entries only.
|
||||||
*/
|
*/
|
||||||
@GetMapping("/entries/today")
|
@GetMapping("/entries/today")
|
||||||
public List<WirdEntry> getTodayEntries() {
|
public List<WirdEntryResponse> getTodayEntries() {
|
||||||
return wirdService.getTodayEntries();
|
return toWirdResponses(wirdService.getTodayEntries());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,10 +63,10 @@ public class WirdController {
|
|||||||
* Used by trend chart to fetch a date window.
|
* Used by trend chart to fetch a date window.
|
||||||
*/
|
*/
|
||||||
@GetMapping("/entries/range")
|
@GetMapping("/entries/range")
|
||||||
public List<WirdEntry> getEntriesInRange(
|
public List<WirdEntryResponse> getEntriesInRange(
|
||||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
|
||||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
|
||||||
return wirdService.getEntriesInRange(from, to);
|
return toWirdResponses(wirdService.getEntriesInRange(from, to));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,11 +74,11 @@ public class WirdController {
|
|||||||
* Filtered by wird type — useful if you want to extend the chart later.
|
* Filtered by wird type — useful if you want to extend the chart later.
|
||||||
*/
|
*/
|
||||||
@GetMapping("/entries/type/{type}")
|
@GetMapping("/entries/type/{type}")
|
||||||
public List<WirdEntry> getEntriesByType(
|
public List<WirdEntryResponse> getEntriesByType(
|
||||||
@PathVariable String type,
|
@PathVariable String type,
|
||||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
|
||||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
|
||||||
return wirdService.getEntriesByTypeInRange(type, from, to);
|
return toWirdResponses(wirdService.getEntriesByTypeInRange(type, from, to));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,10 +87,10 @@ public class WirdController {
|
|||||||
* Creates a new log entry.
|
* Creates a new log entry.
|
||||||
*/
|
*/
|
||||||
@PostMapping("/entries")
|
@PostMapping("/entries")
|
||||||
public ResponseEntity<WirdEntry> createEntry(@RequestBody WirdEntryDTO dto) {
|
public ResponseEntity<WirdEntryResponse> createEntry(@RequestBody WirdEntryDTO dto) {
|
||||||
logger.log(Logger.Level.INFO, "Creating wird entry: {0} on {1}", dto.getWirdType(), dto.getDate());
|
log.info("Creating wird entry type={} date={}", dto.getWirdType(), dto.getDate());
|
||||||
WirdEntry saved = wirdService.createEntry(dto);
|
WirdEntry saved = wirdService.createEntry(dto);
|
||||||
return ResponseEntity.ok(saved);
|
return ResponseEntity.ok(WirdEntryResponse.from(saved));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,7 +99,7 @@ public class WirdController {
|
|||||||
*/
|
*/
|
||||||
@DeleteMapping("/entries/{id}")
|
@DeleteMapping("/entries/{id}")
|
||||||
public ResponseEntity<Void> deleteEntry(@PathVariable Long id) {
|
public ResponseEntity<Void> deleteEntry(@PathVariable Long id) {
|
||||||
logger.log(Logger.Level.INFO, "Deleting wird entry with id: {0}", id);
|
log.info("Deleting wird entry id={}", id);
|
||||||
wirdService.deleteEntry(id);
|
wirdService.deleteEntry(id);
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
@@ -107,8 +109,8 @@ public class WirdController {
|
|||||||
* Returns today's nafl prayer entries (convenience endpoint).
|
* Returns today's nafl prayer entries (convenience endpoint).
|
||||||
*/
|
*/
|
||||||
@GetMapping("/entries/nafl/today")
|
@GetMapping("/entries/nafl/today")
|
||||||
public List<WirdEntry> getNaflToday() {
|
public List<WirdEntryResponse> getNaflToday() {
|
||||||
return wirdService.getNaflForDate(LocalDate.now());
|
return toWirdResponses(wirdService.getNaflForDate(LocalDate.now()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -116,8 +118,8 @@ public class WirdController {
|
|||||||
* All khatm completions, newest first.
|
* All khatm completions, newest first.
|
||||||
*/
|
*/
|
||||||
@GetMapping("/entries/khatm")
|
@GetMapping("/entries/khatm")
|
||||||
public List<WirdEntry> getKhatmEntries() {
|
public List<WirdEntryResponse> getKhatmEntries() {
|
||||||
return wirdService.getKhatmEntries();
|
return toWirdResponses(wirdService.getKhatmEntries());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,9 +127,9 @@ public class WirdController {
|
|||||||
* All study sessions, newest first.
|
* All study sessions, newest first.
|
||||||
*/
|
*/
|
||||||
@GetMapping("/motalah")
|
@GetMapping("/motalah")
|
||||||
public List<MotalahSession> getAll() {
|
public List<MotalahResponse> getAll() {
|
||||||
logger.log(Logger.Level.INFO, "Fetching all motalah sessions");
|
log.debug("Fetching all motalah sessions");
|
||||||
return motalahService.getAll();
|
return toMotalahResponses(motalahService.getAll());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -135,8 +137,8 @@ public class WirdController {
|
|||||||
* Convenience endpoint for today's sessions only.
|
* Convenience endpoint for today's sessions only.
|
||||||
*/
|
*/
|
||||||
@GetMapping("/motalah/today")
|
@GetMapping("/motalah/today")
|
||||||
public List<MotalahSession> getToday() {
|
public List<MotalahResponse> getToday() {
|
||||||
return motalahService.getToday();
|
return toMotalahResponses(motalahService.getToday());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,10 +146,10 @@ public class WirdController {
|
|||||||
* Used by the trend chart and month-total stat.
|
* Used by the trend chart and month-total stat.
|
||||||
*/
|
*/
|
||||||
@GetMapping("/motalah/range")
|
@GetMapping("/motalah/range")
|
||||||
public List<MotalahSession> getInRange(
|
public List<MotalahResponse> getInRange(
|
||||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
|
||||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
|
||||||
return motalahService.getInRange(from, to);
|
return toMotalahResponses(motalahService.getInRange(from, to));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -155,11 +157,10 @@ public class WirdController {
|
|||||||
* Body: { date, durationMinutes, bookIds, notes }
|
* Body: { date, durationMinutes, bookIds, notes }
|
||||||
*/
|
*/
|
||||||
@PostMapping("/motalah")
|
@PostMapping("/motalah")
|
||||||
public ResponseEntity<MotalahSession> create(@RequestBody MotalahSessionDTO dto) {
|
public ResponseEntity<MotalahResponse> create(@RequestBody MotalahSessionDTO dto) {
|
||||||
logger.log(Logger.Level.INFO, "Creating motalah session: {0} min on {1}",
|
log.info("Creating motalah session durationMinutes={} date={}", dto.getDurationMinutes(), dto.getDate());
|
||||||
dto.getDurationMinutes(), dto.getDate());
|
|
||||||
MotalahSession saved = motalahService.create(dto);
|
MotalahSession saved = motalahService.create(dto);
|
||||||
return ResponseEntity.ok(saved);
|
return ResponseEntity.ok(MotalahResponse.from(saved));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -167,9 +168,33 @@ public class WirdController {
|
|||||||
*/
|
*/
|
||||||
@DeleteMapping("/motalah/{id}")
|
@DeleteMapping("/motalah/{id}")
|
||||||
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
||||||
logger.log(Logger.Level.INFO, "Deleting motalah session id: {0}", id);
|
log.info("Deleting motalah session id={}", id);
|
||||||
motalahService.delete(id);
|
motalahService.delete(id);
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<WirdEntryResponse> toWirdResponses(List<WirdEntry> entries) {
|
||||||
|
return entries.stream().map(WirdEntryResponse::from).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<MotalahResponse> toMotalahResponses(List<MotalahSession> sessions) {
|
||||||
|
return sessions.stream().map(MotalahResponse::from).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record WirdEntryResponse(Long id, String wirdType, LocalDate date, java.math.BigDecimal value,
|
||||||
|
String notes, java.time.OffsetDateTime createdAt) {
|
||||||
|
static WirdEntryResponse from(WirdEntry entry) {
|
||||||
|
return new WirdEntryResponse(entry.getId(), entry.getWirdType(), entry.getDate(), entry.getValue(),
|
||||||
|
entry.getNotes(), entry.getCreatedAt());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record MotalahResponse(Long id, LocalDate date, Integer durationMinutes, List<Integer> bookIds,
|
||||||
|
String notes, java.time.OffsetDateTime createdAt) {
|
||||||
|
static MotalahResponse from(MotalahSession session) {
|
||||||
|
return new MotalahResponse(session.getId(), session.getDate(), session.getDurationMinutes(),
|
||||||
|
session.getBookIds(), session.getNotes(), session.getCreatedAt());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.zaine.app.config.BuildProperties;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.zaine.app.service.BuildRunStateService;
|
import org.zaine.app.service.BuildRunStateService;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -67,31 +66,41 @@ public class BuildController {
|
|||||||
private final AtomicReference<Process> resourceProcess = new AtomicReference<>();
|
private final AtomicReference<Process> resourceProcess = new AtomicReference<>();
|
||||||
private final AtomicReference<Process> guacamoleStartProcess = new AtomicReference<>();
|
private final AtomicReference<Process> guacamoleStartProcess = new AtomicReference<>();
|
||||||
private final AtomicReference<Process> guacamoleStopProcess = new AtomicReference<>();
|
private final AtomicReference<Process> guacamoleStopProcess = new AtomicReference<>();
|
||||||
private final AtomicReference<Process> nostalgiaProcess = new AtomicReference<>();
|
|
||||||
|
|
||||||
@Autowired
|
public BuildController(BuildRunStateService buildRunState, BuildProperties properties) {
|
||||||
public BuildController(BuildRunStateService buildRunState) {
|
|
||||||
this.buildRunState = buildRunState;
|
this.buildRunState = buildRunState;
|
||||||
|
this.webBuildDirectory = properties.webDirectory();
|
||||||
|
this.webBuildLogFile = properties.webLog();
|
||||||
|
this.emacsRunDirectory = properties.emacsDirectory();
|
||||||
|
this.emacsRunLogFile = properties.emacsLog();
|
||||||
|
this.combinedRunLogFile = properties.combinedLog();
|
||||||
|
this.resourceFreeDirectory = properties.resourceDirectory();
|
||||||
|
this.resourceFreeLogFile = properties.resourceLog();
|
||||||
|
this.resourceFreeCommand = properties.resourceCommand();
|
||||||
|
this.guacamoleRunDirectory = properties.guacamoleDirectory();
|
||||||
|
this.guacamoleContainerName = properties.guacamoleContainer();
|
||||||
|
this.guacamoleStartLogFile = properties.guacamoleStartLog();
|
||||||
|
this.guacamoleStopLogFile = properties.guacamoleStopLog();
|
||||||
|
this.nostalgiaRunLogFile = properties.nostalgiaLog();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================================================
|
/* =========================================================
|
||||||
CONFIG
|
CONFIG
|
||||||
========================================================= */
|
========================================================= */
|
||||||
|
|
||||||
@Value("${zone.build.dir}") private String webBuildDirectory;
|
private final String webBuildDirectory;
|
||||||
@Value("${zone.build.log}") private String webBuildLogFile;
|
private final String webBuildLogFile;
|
||||||
@Value("${emacs.run.dir}") private String emacsRunDirectory;
|
private final String emacsRunDirectory;
|
||||||
@Value("${emacs.run.log}") private String emacsRunLogFile;
|
private final String emacsRunLogFile;
|
||||||
@Value("${combined.run.log}") private String combinedRunLogFile;
|
private final String combinedRunLogFile;
|
||||||
@Value("${adventure.resources.free.dir:/home/zaine}") private String resourceFreeDirectory;
|
private final String resourceFreeDirectory;
|
||||||
@Value("${adventure.resources.free.log:/home/zaine/logs/adventure-resources.log}") private String resourceFreeLogFile;
|
private final String resourceFreeLogFile;
|
||||||
@Value("${adventure.resources.free.command:sudo -n sh -c 'sync; echo 3 > /proc/sys/vm/drop_caches' && /usr/bin/docker system prune -af && /usr/bin/docker builder prune -af}") private String resourceFreeCommand;
|
private final String resourceFreeCommand;
|
||||||
@Value("${guacamole.run.dir:/home/zaine}") private String guacamoleRunDirectory;
|
private final String guacamoleRunDirectory;
|
||||||
@Value("${guacamole.container.name:guacamole}") private String guacamoleContainerName;
|
private final String guacamoleContainerName;
|
||||||
@Value("${guacamole.start.log:/home/zaine/logs/guacamole-start.log}") private String guacamoleStartLogFile;
|
private final String guacamoleStartLogFile;
|
||||||
@Value("${guacamole.stop.log:/home/zaine/logs/guacamole-stop.log}") private String guacamoleStopLogFile;
|
private final String guacamoleStopLogFile;
|
||||||
@Value("${nostalgia.run.dir:/home/zaine/master-folder/projects/_personal/nostalgia}") private String nostalgiaRunDirectory;
|
private final String nostalgiaRunLogFile;
|
||||||
@Value("${nostalgia.run.log:/home/zaine/logs/nostalgia-prod.log}") private String nostalgiaRunLogFile;
|
|
||||||
|
|
||||||
/* =========================================================
|
/* =========================================================
|
||||||
HEALTH + UPTIME
|
HEALTH + UPTIME
|
||||||
@@ -356,31 +365,6 @@ public class BuildController {
|
|||||||
return streamLogs(guacamoleStopLogFile);
|
return streamLogs(guacamoleStopLogFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Run Nostalgia production container",
|
|
||||||
description = "Runs docker compose up -d --build in the Nostalgia project directory"
|
|
||||||
)
|
|
||||||
@PostMapping("/nostalgia/production")
|
|
||||||
public ResponseEntity<String> runNostalgiaProduction() {
|
|
||||||
return startCommand(
|
|
||||||
BuildRunStateService.NOSTALGIA_PROD,
|
|
||||||
"nostalgia production",
|
|
||||||
nostalgiaRunDirectory,
|
|
||||||
nostalgiaRunLogFile,
|
|
||||||
List.of("docker", "compose", "up", "-d", "--build"),
|
|
||||||
nostalgiaProcess
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/nostalgia/production")
|
|
||||||
public ResponseEntity<String> cancelNostalgiaProduction() {
|
|
||||||
return killProcess(
|
|
||||||
nostalgiaProcess,
|
|
||||||
BuildRunStateService.NOSTALGIA_PROD,
|
|
||||||
"nostalgia production"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/nostalgia/production/status")
|
@GetMapping("/nostalgia/production/status")
|
||||||
public ResponseEntity<BuildStatus> getNostalgiaProductionStatus() {
|
public ResponseEntity<BuildStatus> getNostalgiaProductionStatus() {
|
||||||
var s = buildRunState.status(BuildRunStateService.NOSTALGIA_PROD);
|
var s = buildRunState.status(BuildRunStateService.NOSTALGIA_PROD);
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ package org.zaine.app.controller.zone;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.zaine.app.common.application.ApplicationException;
|
||||||
import org.zaine.app.service.ZoneManifestService;
|
import org.zaine.app.service.ZoneManifestService;
|
||||||
import org.zaine.app.service.ZoneStatusService;
|
import org.zaine.app.service.ZoneStatusService;
|
||||||
|
|
||||||
@@ -34,7 +32,7 @@ public class ZoneController {
|
|||||||
try {
|
try {
|
||||||
return manifestService.loadEnrichedManifest();
|
return manifestService.loadEnrichedManifest();
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage());
|
throw ApplicationException.failure(ex.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package org.zaine.app.dto;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lightweight projection of a Calibre book — just what the
|
|
||||||
* Mutāla'ah book-picker needs.
|
|
||||||
*/
|
|
||||||
public class CalibreBookDTO {
|
|
||||||
|
|
||||||
private final long id;
|
|
||||||
private final String title;
|
|
||||||
private final String authors;
|
|
||||||
|
|
||||||
public CalibreBookDTO(long id, String title, String authors) {
|
|
||||||
this.id = id;
|
|
||||||
this.title = title;
|
|
||||||
this.authors = authors != null ? authors : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getId() { return id; }
|
|
||||||
public String getTitle() { return title; }
|
|
||||||
public String getAuthors() { return authors; }
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package org.zaine.app.dto;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
|
|
||||||
public class CreateNoteDTO {
|
|
||||||
|
|
||||||
@JsonProperty("author_name")
|
|
||||||
private String authorName;
|
|
||||||
|
|
||||||
private String content;
|
|
||||||
|
|
||||||
public CreateNoteDTO() {}
|
|
||||||
|
|
||||||
public String getAuthorName() {
|
|
||||||
return authorName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAuthorName(String authorName) {
|
|
||||||
this.authorName = authorName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getContent() {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setContent(String content) {
|
|
||||||
this.content = content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package org.zaine.app.dto;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
|
|
||||||
public record EveningCheckinRequest(
|
|
||||||
LocalDate date,
|
|
||||||
Integer mood,
|
|
||||||
Integer stressLevel,
|
|
||||||
Boolean duties,
|
|
||||||
Boolean zikr,
|
|
||||||
Boolean salah,
|
|
||||||
String reflection,
|
|
||||||
String bestThingToday) {
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package org.zaine.app.dto;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
|
|
||||||
public record MorningCheckinRequest(
|
|
||||||
LocalDate date,
|
|
||||||
BigDecimal sleepHours,
|
|
||||||
Integer energyLevel,
|
|
||||||
Integer mood,
|
|
||||||
Boolean fajr,
|
|
||||||
Boolean quran,
|
|
||||||
Boolean exercise,
|
|
||||||
String note) {
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
package org.zaine.app.dto;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public final class PeriodCompanionDTO {
|
|
||||||
private PeriodCompanionDTO() {}
|
|
||||||
|
|
||||||
public record SymptomDTO(Long id, String name) {}
|
|
||||||
|
|
||||||
public record DailyLogDTO(
|
|
||||||
Long id,
|
|
||||||
LocalDate date,
|
|
||||||
String flowLevel,
|
|
||||||
String mood,
|
|
||||||
String energyLevel,
|
|
||||||
String notes,
|
|
||||||
List<SymptomDTO> symptoms,
|
|
||||||
OffsetDateTime createdAt,
|
|
||||||
OffsetDateTime updatedAt) {}
|
|
||||||
|
|
||||||
public record DailyLogRequest(
|
|
||||||
LocalDate date,
|
|
||||||
String flowLevel,
|
|
||||||
String mood,
|
|
||||||
String energyLevel,
|
|
||||||
String notes,
|
|
||||||
List<String> symptoms) {}
|
|
||||||
|
|
||||||
public record PeriodCycleDTO(
|
|
||||||
Long id,
|
|
||||||
LocalDate startDate,
|
|
||||||
LocalDate endDate,
|
|
||||||
Integer cycleLength,
|
|
||||||
Integer periodLength,
|
|
||||||
OffsetDateTime createdAt,
|
|
||||||
OffsetDateTime updatedAt) {}
|
|
||||||
|
|
||||||
public record PeriodCycleRequest(LocalDate startDate, LocalDate endDate) {}
|
|
||||||
|
|
||||||
public record PeriodEntryRequest(LocalDate startDate, LocalDate endDate, String flowLevel) {}
|
|
||||||
|
|
||||||
public record PredictionDTO(
|
|
||||||
Long id,
|
|
||||||
OffsetDateTime generatedAt,
|
|
||||||
LocalDate predictedPeriodDate,
|
|
||||||
LocalDate predictedOvulationDate,
|
|
||||||
LocalDate fertileWindowStart,
|
|
||||||
LocalDate fertileWindowEnd,
|
|
||||||
int averageCycleLength,
|
|
||||||
int averagePeriodLength) {}
|
|
||||||
|
|
||||||
public record CurrentCycleDTO(
|
|
||||||
Integer day,
|
|
||||||
String phase,
|
|
||||||
PeriodCycleDTO cycle,
|
|
||||||
int averageCycleLength,
|
|
||||||
int averagePeriodLength) {}
|
|
||||||
|
|
||||||
public record DashboardDTO(
|
|
||||||
CurrentCycleDTO currentCycle,
|
|
||||||
PredictionDTO prediction,
|
|
||||||
DailyLogDTO today,
|
|
||||||
SettingsDTO settings,
|
|
||||||
List<String> supportMessages,
|
|
||||||
List<SupportMessageDTO> recentPartnerMessages) {}
|
|
||||||
|
|
||||||
public record CalendarDayDTO(
|
|
||||||
LocalDate date,
|
|
||||||
boolean periodDay,
|
|
||||||
boolean predictedPeriodDay,
|
|
||||||
boolean fertileWindow,
|
|
||||||
boolean ovulationDay,
|
|
||||||
List<String> symptoms,
|
|
||||||
DailyLogDTO log) {}
|
|
||||||
|
|
||||||
public record CycleTrendDTO(LocalDate startDate, Integer cycleLength, Integer periodLength) {}
|
|
||||||
public record SymptomTrendDTO(String symptom, long count) {}
|
|
||||||
public record MoodTrendDTO(LocalDate date, String mood) {}
|
|
||||||
|
|
||||||
public record InsightsDTO(
|
|
||||||
List<CycleTrendDTO> cycleTrends,
|
|
||||||
List<SymptomTrendDTO> symptomTrends,
|
|
||||||
List<MoodTrendDTO> moodTrends) {}
|
|
||||||
|
|
||||||
public record SupportMessageDTO(Long id, String authorName, String message, OffsetDateTime createdAt) {}
|
|
||||||
public record SupportMessageRequest(String authorName, String message) {}
|
|
||||||
public record CreateSymptomRequest(String name) {}
|
|
||||||
public record SettingsDTO(int averageCycleLength, int averagePeriodLength, OffsetDateTime updatedAt) {}
|
|
||||||
public record SettingsRequest(Integer averageCycleLength, Integer averagePeriodLength) {}
|
|
||||||
public record FastingLogDTO(Long id, LocalDate fastDate, String status, String notes, OffsetDateTime createdAt, OffsetDateTime updatedAt) {}
|
|
||||||
public record FastingLogRequest(LocalDate fastDate, String status, String notes) {}
|
|
||||||
public record ReflectionDTO(Long id, LocalDate date, String prompt, String response, OffsetDateTime createdAt, OffsetDateTime updatedAt) {}
|
|
||||||
public record ReflectionRequest(LocalDate date, String prompt, String response) {}
|
|
||||||
public record WellbeingDTO(
|
|
||||||
String phase,
|
|
||||||
String bodyInsight,
|
|
||||||
String supportiveAction,
|
|
||||||
String spiritualReminder,
|
|
||||||
String fiqhNote,
|
|
||||||
ReflectionDTO todayReflection,
|
|
||||||
List<ReflectionDTO> recentReflections,
|
|
||||||
List<FastingLogDTO> fastingLogs,
|
|
||||||
long missedFastCount,
|
|
||||||
long madeUpFastCount) {}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package org.zaine.app.dto;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
|
|
||||||
public record TodayStatusDTO(
|
|
||||||
LocalDate date,
|
|
||||||
boolean morningDone,
|
|
||||||
boolean eveningDone,
|
|
||||||
BigDecimal sleepHours,
|
|
||||||
Integer morningEnergyLevel,
|
|
||||||
Integer morningMoodLevel,
|
|
||||||
Integer eveningMoodLevel,
|
|
||||||
Integer stressLevel,
|
|
||||||
Boolean fajr,
|
|
||||||
Boolean quran,
|
|
||||||
Boolean exercise,
|
|
||||||
Boolean duties,
|
|
||||||
Boolean zikr,
|
|
||||||
Boolean salah,
|
|
||||||
String reflection,
|
|
||||||
OffsetDateTime morningCompletedAt,
|
|
||||||
OffsetDateTime eveningCompletedAt) {
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,23 @@
|
|||||||
package org.zaine.app.exception;
|
package org.zaine.app.exception;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.dao.DataAccessException;
|
import org.springframework.dao.DataAccessException;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
|
||||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||||
|
import org.zaine.app.common.application.ApplicationException;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||||
|
|
||||||
@ExceptionHandler(NoHandlerFoundException.class)
|
@ExceptionHandler(NoHandlerFoundException.class)
|
||||||
public ResponseEntity<Map<String, String>> handleNotFound(HttpServletRequest request) {
|
public ResponseEntity<Map<String, String>> handleNotFound(HttpServletRequest request) {
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(
|
||||||
@@ -21,24 +25,30 @@ public class GlobalExceptionHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(ResponseStatusException.class)
|
@ExceptionHandler(ApplicationException.class)
|
||||||
public ResponseEntity<Map<String, String>> handleResponseStatus(
|
public ResponseEntity<Map<String, String>> handleApplication(
|
||||||
ResponseStatusException ex,
|
ApplicationException ex,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
HttpStatus status = HttpStatus.resolve(ex.getStatusCode().value());
|
HttpStatus status = switch (ex.kind()) {
|
||||||
if (status == null) {
|
case BAD_REQUEST -> HttpStatus.BAD_REQUEST;
|
||||||
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
case NOT_FOUND -> HttpStatus.NOT_FOUND;
|
||||||
|
case UNAUTHORIZED -> HttpStatus.UNAUTHORIZED;
|
||||||
|
case PAYLOAD_TOO_LARGE -> HttpStatus.PAYLOAD_TOO_LARGE;
|
||||||
|
case FAILURE -> HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
};
|
||||||
|
if (status.is4xxClientError()) {
|
||||||
|
log.debug("Request rejected: status={}, path={}, reason={}", status.value(), request.getRequestURI(), ex.getMessage());
|
||||||
|
} else {
|
||||||
|
log.warn("Application operation failed: path={}, reason={}", request.getRequestURI(), ex.getMessage());
|
||||||
}
|
}
|
||||||
String reason = ex.getReason() != null ? ex.getReason() : status.getReasonPhrase();
|
return ResponseEntity.status(status).body(Map.of("message", ex.getMessage(), "path", request.getRequestURI()));
|
||||||
return ResponseEntity.status(status).body(
|
|
||||||
Map.of("message", reason, "path", request.getRequestURI())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(DataAccessException.class)
|
@ExceptionHandler(DataAccessException.class)
|
||||||
public ResponseEntity<Map<String, String>> handleDataAccess(
|
public ResponseEntity<Map<String, String>> handleDataAccess(
|
||||||
DataAccessException ex,
|
DataAccessException ex,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
|
log.error("Database operation failed for path {}", request.getRequestURI(), ex);
|
||||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(
|
||||||
Map.of(
|
Map.of(
|
||||||
"message", "Database error",
|
"message", "Database error",
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
package org.zaine.app.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
|
||||||
import jakarta.persistence.GenerationType;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.PrePersist;
|
|
||||||
import jakarta.persistence.PreUpdate;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "daily_checkins")
|
|
||||||
public class DailyCheckin {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(nullable = false, unique = true)
|
|
||||||
private LocalDate date;
|
|
||||||
|
|
||||||
@Column(name = "sleep_hours", precision = 4, scale = 2)
|
|
||||||
private BigDecimal sleepHours;
|
|
||||||
|
|
||||||
@Column(name = "morning_energy_level")
|
|
||||||
private Integer morningEnergyLevel;
|
|
||||||
|
|
||||||
@Column(name = "morning_mood_level")
|
|
||||||
private Integer morningMoodLevel;
|
|
||||||
|
|
||||||
@Column(name = "morning_note")
|
|
||||||
private String morningNote;
|
|
||||||
|
|
||||||
@Column(name = "fajr")
|
|
||||||
private Boolean fajr;
|
|
||||||
|
|
||||||
@Column(name = "quran")
|
|
||||||
private Boolean quran;
|
|
||||||
|
|
||||||
@Column(name = "exercise")
|
|
||||||
private Boolean exercise;
|
|
||||||
|
|
||||||
@Column(name = "morning_completed_at")
|
|
||||||
private OffsetDateTime morningCompletedAt;
|
|
||||||
|
|
||||||
@Column(name = "evening_mood_level")
|
|
||||||
private Integer eveningMoodLevel;
|
|
||||||
|
|
||||||
@Column(name = "stress_level")
|
|
||||||
private Integer stressLevel;
|
|
||||||
|
|
||||||
@Column(name = "reflection", columnDefinition = "text")
|
|
||||||
private String reflection;
|
|
||||||
|
|
||||||
@Column(name = "best_thing_today")
|
|
||||||
private String bestThingToday;
|
|
||||||
|
|
||||||
@Column(name = "duties")
|
|
||||||
private Boolean duties;
|
|
||||||
|
|
||||||
@Column(name = "zikr")
|
|
||||||
private Boolean zikr;
|
|
||||||
|
|
||||||
@Column(name = "salah")
|
|
||||||
private Boolean salah;
|
|
||||||
|
|
||||||
@Column(name = "evening_completed_at")
|
|
||||||
private OffsetDateTime eveningCompletedAt;
|
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false, updatable = false)
|
|
||||||
private OffsetDateTime createdAt;
|
|
||||||
|
|
||||||
@Column(name = "updated_at", nullable = false)
|
|
||||||
private OffsetDateTime updatedAt;
|
|
||||||
|
|
||||||
@PrePersist
|
|
||||||
void onInsert() {
|
|
||||||
OffsetDateTime now = OffsetDateTime.now();
|
|
||||||
this.createdAt = now;
|
|
||||||
this.updatedAt = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreUpdate
|
|
||||||
void onUpdate() {
|
|
||||||
this.updatedAt = OffsetDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getId() { return id; }
|
|
||||||
public LocalDate getDate() { return date; }
|
|
||||||
public void setDate(LocalDate date) { this.date = date; }
|
|
||||||
public BigDecimal getSleepHours() { return sleepHours; }
|
|
||||||
public void setSleepHours(BigDecimal sleepHours) { this.sleepHours = sleepHours; }
|
|
||||||
public Integer getMorningEnergyLevel() { return morningEnergyLevel; }
|
|
||||||
public void setMorningEnergyLevel(Integer morningEnergyLevel) { this.morningEnergyLevel = morningEnergyLevel; }
|
|
||||||
public Integer getMorningMoodLevel() { return morningMoodLevel; }
|
|
||||||
public void setMorningMoodLevel(Integer morningMoodLevel) { this.morningMoodLevel = morningMoodLevel; }
|
|
||||||
public String getMorningNote() { return morningNote; }
|
|
||||||
public void setMorningNote(String morningNote) { this.morningNote = morningNote; }
|
|
||||||
public Boolean getFajr() { return fajr; }
|
|
||||||
public void setFajr(Boolean fajr) { this.fajr = fajr; }
|
|
||||||
public Boolean getQuran() { return quran; }
|
|
||||||
public void setQuran(Boolean quran) { this.quran = quran; }
|
|
||||||
public Boolean getExercise() { return exercise; }
|
|
||||||
public void setExercise(Boolean exercise) { this.exercise = exercise; }
|
|
||||||
public OffsetDateTime getMorningCompletedAt() { return morningCompletedAt; }
|
|
||||||
public void setMorningCompletedAt(OffsetDateTime morningCompletedAt) { this.morningCompletedAt = morningCompletedAt; }
|
|
||||||
public Integer getEveningMoodLevel() { return eveningMoodLevel; }
|
|
||||||
public void setEveningMoodLevel(Integer eveningMoodLevel) { this.eveningMoodLevel = eveningMoodLevel; }
|
|
||||||
public Integer getStressLevel() { return stressLevel; }
|
|
||||||
public void setStressLevel(Integer stressLevel) { this.stressLevel = stressLevel; }
|
|
||||||
public String getReflection() { return reflection; }
|
|
||||||
public void setReflection(String reflection) { this.reflection = reflection; }
|
|
||||||
public String getBestThingToday() { return bestThingToday; }
|
|
||||||
public void setBestThingToday(String bestThingToday) { this.bestThingToday = bestThingToday; }
|
|
||||||
public Boolean getDuties() { return duties; }
|
|
||||||
public void setDuties(Boolean duties) { this.duties = duties; }
|
|
||||||
public Boolean getZikr() { return zikr; }
|
|
||||||
public void setZikr(Boolean zikr) { this.zikr = zikr; }
|
|
||||||
public Boolean getSalah() { return salah; }
|
|
||||||
public void setSalah(Boolean salah) { this.salah = salah; }
|
|
||||||
public OffsetDateTime getEveningCompletedAt() { return eveningCompletedAt; }
|
|
||||||
public void setEveningCompletedAt(OffsetDateTime eveningCompletedAt) { this.eveningCompletedAt = eveningCompletedAt; }
|
|
||||||
public OffsetDateTime getCreatedAt() { return createdAt; }
|
|
||||||
public OffsetDateTime getUpdatedAt() { return updatedAt; }
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package org.zaine.app.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import org.hibernate.annotations.CreationTimestamp;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "public_notes")
|
|
||||||
public class Notes {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue
|
|
||||||
@Column(name="id")
|
|
||||||
private UUID id;
|
|
||||||
|
|
||||||
@Column(name="content")
|
|
||||||
private String content;
|
|
||||||
|
|
||||||
@JsonProperty("author_name")
|
|
||||||
@Column(name="author_name")
|
|
||||||
private String authorName;
|
|
||||||
|
|
||||||
@JsonProperty("created_at")
|
|
||||||
|
|
||||||
@CreationTimestamp
|
|
||||||
@Column(name = "created_at", nullable = false, updatable = false)
|
|
||||||
private Instant createdAt;
|
|
||||||
|
|
||||||
|
|
||||||
public UUID getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getContent() {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setContent(String content) {
|
|
||||||
this.content = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAuthorName() {
|
|
||||||
return authorName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAuthorName(String authorName) {
|
|
||||||
this.authorName = authorName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Instant getCreatedAt() {
|
|
||||||
return createdAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCreatedAt(Instant createdAt) {
|
|
||||||
this.createdAt = createdAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package org.zaine.app.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
|
||||||
import jakarta.persistence.GenerationType;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.PrePersist;
|
|
||||||
import jakarta.persistence.PreUpdate;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "pc_cycles")
|
|
||||||
public class PeriodCycle {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(name = "start_date", nullable = false, unique = true)
|
|
||||||
private LocalDate startDate;
|
|
||||||
|
|
||||||
@Column(name = "end_date")
|
|
||||||
private LocalDate endDate;
|
|
||||||
|
|
||||||
@Column(name = "cycle_length")
|
|
||||||
private Integer cycleLength;
|
|
||||||
|
|
||||||
@Column(name = "period_length")
|
|
||||||
private Integer periodLength;
|
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false, updatable = false)
|
|
||||||
private OffsetDateTime createdAt;
|
|
||||||
|
|
||||||
@Column(name = "updated_at", nullable = false)
|
|
||||||
private OffsetDateTime updatedAt;
|
|
||||||
|
|
||||||
@PrePersist
|
|
||||||
void onInsert() {
|
|
||||||
OffsetDateTime now = OffsetDateTime.now();
|
|
||||||
this.createdAt = now;
|
|
||||||
this.updatedAt = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreUpdate
|
|
||||||
void onUpdate() {
|
|
||||||
this.updatedAt = OffsetDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getId() { return id; }
|
|
||||||
public LocalDate getStartDate() { return startDate; }
|
|
||||||
public void setStartDate(LocalDate startDate) { this.startDate = startDate; }
|
|
||||||
public LocalDate getEndDate() { return endDate; }
|
|
||||||
public void setEndDate(LocalDate endDate) { this.endDate = endDate; }
|
|
||||||
public Integer getCycleLength() { return cycleLength; }
|
|
||||||
public void setCycleLength(Integer cycleLength) { this.cycleLength = cycleLength; }
|
|
||||||
public Integer getPeriodLength() { return periodLength; }
|
|
||||||
public void setPeriodLength(Integer periodLength) { this.periodLength = periodLength; }
|
|
||||||
public OffsetDateTime getCreatedAt() { return createdAt; }
|
|
||||||
public OffsetDateTime getUpdatedAt() { return updatedAt; }
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package org.zaine.app.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.FetchType;
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
|
||||||
import jakarta.persistence.GenerationType;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.JoinColumn;
|
|
||||||
import jakarta.persistence.JoinTable;
|
|
||||||
import jakarta.persistence.ManyToMany;
|
|
||||||
import jakarta.persistence.PrePersist;
|
|
||||||
import jakarta.persistence.PreUpdate;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "pc_daily_logs")
|
|
||||||
public class PeriodDailyLog {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(nullable = false, unique = true)
|
|
||||||
private LocalDate date;
|
|
||||||
|
|
||||||
@Column(name = "flow_level")
|
|
||||||
private String flowLevel;
|
|
||||||
|
|
||||||
@Column(name = "mood")
|
|
||||||
private String mood;
|
|
||||||
|
|
||||||
@Column(name = "energy_level")
|
|
||||||
private String energyLevel;
|
|
||||||
|
|
||||||
@Column(name = "notes", columnDefinition = "text")
|
|
||||||
private String notes;
|
|
||||||
|
|
||||||
@ManyToMany(fetch = FetchType.EAGER)
|
|
||||||
@JoinTable(
|
|
||||||
name = "pc_daily_log_symptoms",
|
|
||||||
joinColumns = @JoinColumn(name = "daily_log_id"),
|
|
||||||
inverseJoinColumns = @JoinColumn(name = "symptom_id"))
|
|
||||||
private Set<PeriodSymptom> symptoms = new LinkedHashSet<>();
|
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false, updatable = false)
|
|
||||||
private OffsetDateTime createdAt;
|
|
||||||
|
|
||||||
@Column(name = "updated_at", nullable = false)
|
|
||||||
private OffsetDateTime updatedAt;
|
|
||||||
|
|
||||||
@PrePersist
|
|
||||||
void onInsert() {
|
|
||||||
OffsetDateTime now = OffsetDateTime.now();
|
|
||||||
this.createdAt = now;
|
|
||||||
this.updatedAt = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreUpdate
|
|
||||||
void onUpdate() {
|
|
||||||
this.updatedAt = OffsetDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getId() { return id; }
|
|
||||||
public LocalDate getDate() { return date; }
|
|
||||||
public void setDate(LocalDate date) { this.date = date; }
|
|
||||||
public String getFlowLevel() { return flowLevel; }
|
|
||||||
public void setFlowLevel(String flowLevel) { this.flowLevel = flowLevel; }
|
|
||||||
public String getMood() { return mood; }
|
|
||||||
public void setMood(String mood) { this.mood = mood; }
|
|
||||||
public String getEnergyLevel() { return energyLevel; }
|
|
||||||
public void setEnergyLevel(String energyLevel) { this.energyLevel = energyLevel; }
|
|
||||||
public String getNotes() { return notes; }
|
|
||||||
public void setNotes(String notes) { this.notes = notes; }
|
|
||||||
public Set<PeriodSymptom> getSymptoms() { return symptoms; }
|
|
||||||
public void setSymptoms(Set<PeriodSymptom> symptoms) { this.symptoms = symptoms; }
|
|
||||||
public OffsetDateTime getCreatedAt() { return createdAt; }
|
|
||||||
public OffsetDateTime getUpdatedAt() { return updatedAt; }
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package org.zaine.app.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
|
||||||
import jakarta.persistence.GenerationType;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.PrePersist;
|
|
||||||
import jakarta.persistence.PreUpdate;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "pc_daily_reflections")
|
|
||||||
public class PeriodDailyReflection {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(nullable = false, unique = true)
|
|
||||||
private LocalDate date;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private String prompt;
|
|
||||||
|
|
||||||
@Column(name = "response", columnDefinition = "text")
|
|
||||||
private String response;
|
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false, updatable = false)
|
|
||||||
private OffsetDateTime createdAt;
|
|
||||||
|
|
||||||
@Column(name = "updated_at", nullable = false)
|
|
||||||
private OffsetDateTime updatedAt;
|
|
||||||
|
|
||||||
@PrePersist
|
|
||||||
void onInsert() {
|
|
||||||
OffsetDateTime now = OffsetDateTime.now();
|
|
||||||
this.createdAt = now;
|
|
||||||
this.updatedAt = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreUpdate
|
|
||||||
void onUpdate() {
|
|
||||||
this.updatedAt = OffsetDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getId() { return id; }
|
|
||||||
public LocalDate getDate() { return date; }
|
|
||||||
public void setDate(LocalDate date) { this.date = date; }
|
|
||||||
public String getPrompt() { return prompt; }
|
|
||||||
public void setPrompt(String prompt) { this.prompt = prompt; }
|
|
||||||
public String getResponse() { return response; }
|
|
||||||
public void setResponse(String response) { this.response = response; }
|
|
||||||
public OffsetDateTime getCreatedAt() { return createdAt; }
|
|
||||||
public OffsetDateTime getUpdatedAt() { return updatedAt; }
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package org.zaine.app.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
|
||||||
import jakarta.persistence.GenerationType;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.PrePersist;
|
|
||||||
import jakarta.persistence.PreUpdate;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "pc_fasting_logs")
|
|
||||||
public class PeriodFastingLog {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(name = "fast_date", nullable = false, unique = true)
|
|
||||||
private LocalDate fastDate;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private String status;
|
|
||||||
|
|
||||||
@Column(name = "notes", columnDefinition = "text")
|
|
||||||
private String notes;
|
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false, updatable = false)
|
|
||||||
private OffsetDateTime createdAt;
|
|
||||||
|
|
||||||
@Column(name = "updated_at", nullable = false)
|
|
||||||
private OffsetDateTime updatedAt;
|
|
||||||
|
|
||||||
@PrePersist
|
|
||||||
void onInsert() {
|
|
||||||
OffsetDateTime now = OffsetDateTime.now();
|
|
||||||
this.createdAt = now;
|
|
||||||
this.updatedAt = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreUpdate
|
|
||||||
void onUpdate() {
|
|
||||||
this.updatedAt = OffsetDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getId() { return id; }
|
|
||||||
public LocalDate getFastDate() { return fastDate; }
|
|
||||||
public void setFastDate(LocalDate fastDate) { this.fastDate = fastDate; }
|
|
||||||
public String getStatus() { return status; }
|
|
||||||
public void setStatus(String status) { this.status = status; }
|
|
||||||
public String getNotes() { return notes; }
|
|
||||||
public void setNotes(String notes) { this.notes = notes; }
|
|
||||||
public OffsetDateTime getCreatedAt() { return createdAt; }
|
|
||||||
public OffsetDateTime getUpdatedAt() { return updatedAt; }
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package org.zaine.app.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
|
||||||
import jakarta.persistence.GenerationType;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.PrePersist;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "pc_prediction_snapshots")
|
|
||||||
public class PeriodPredictionSnapshot {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(name = "generated_at", nullable = false, updatable = false)
|
|
||||||
private OffsetDateTime generatedAt;
|
|
||||||
|
|
||||||
@Column(name = "predicted_period_date", nullable = false)
|
|
||||||
private LocalDate predictedPeriodDate;
|
|
||||||
|
|
||||||
@Column(name = "predicted_ovulation_date", nullable = false)
|
|
||||||
private LocalDate predictedOvulationDate;
|
|
||||||
|
|
||||||
@Column(name = "fertile_window_start", nullable = false)
|
|
||||||
private LocalDate fertileWindowStart;
|
|
||||||
|
|
||||||
@Column(name = "fertile_window_end", nullable = false)
|
|
||||||
private LocalDate fertileWindowEnd;
|
|
||||||
|
|
||||||
@PrePersist
|
|
||||||
void onInsert() {
|
|
||||||
this.generatedAt = OffsetDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getId() { return id; }
|
|
||||||
public OffsetDateTime getGeneratedAt() { return generatedAt; }
|
|
||||||
public void setGeneratedAt(OffsetDateTime generatedAt) { this.generatedAt = generatedAt; }
|
|
||||||
public LocalDate getPredictedPeriodDate() { return predictedPeriodDate; }
|
|
||||||
public void setPredictedPeriodDate(LocalDate predictedPeriodDate) { this.predictedPeriodDate = predictedPeriodDate; }
|
|
||||||
public LocalDate getPredictedOvulationDate() { return predictedOvulationDate; }
|
|
||||||
public void setPredictedOvulationDate(LocalDate predictedOvulationDate) { this.predictedOvulationDate = predictedOvulationDate; }
|
|
||||||
public LocalDate getFertileWindowStart() { return fertileWindowStart; }
|
|
||||||
public void setFertileWindowStart(LocalDate fertileWindowStart) { this.fertileWindowStart = fertileWindowStart; }
|
|
||||||
public LocalDate getFertileWindowEnd() { return fertileWindowEnd; }
|
|
||||||
public void setFertileWindowEnd(LocalDate fertileWindowEnd) { this.fertileWindowEnd = fertileWindowEnd; }
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package org.zaine.app.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.PrePersist;
|
|
||||||
import jakarta.persistence.PreUpdate;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "pc_settings")
|
|
||||||
public class PeriodSettings {
|
|
||||||
public static final Short SINGLETON_ID = 1;
|
|
||||||
|
|
||||||
@Id
|
|
||||||
private Short id = SINGLETON_ID;
|
|
||||||
|
|
||||||
@Column(name = "average_cycle_length", nullable = false)
|
|
||||||
private Integer averageCycleLength = 28;
|
|
||||||
|
|
||||||
@Column(name = "average_period_length", nullable = false)
|
|
||||||
private Integer averagePeriodLength = 5;
|
|
||||||
|
|
||||||
@Column(name = "updated_at", nullable = false)
|
|
||||||
private OffsetDateTime updatedAt;
|
|
||||||
|
|
||||||
@PrePersist
|
|
||||||
void onInsert() {
|
|
||||||
this.updatedAt = OffsetDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreUpdate
|
|
||||||
void onUpdate() {
|
|
||||||
this.updatedAt = OffsetDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Short getId() { return id; }
|
|
||||||
public void setId(Short id) { this.id = id; }
|
|
||||||
public Integer getAverageCycleLength() { return averageCycleLength; }
|
|
||||||
public void setAverageCycleLength(Integer averageCycleLength) { this.averageCycleLength = averageCycleLength; }
|
|
||||||
public Integer getAveragePeriodLength() { return averagePeriodLength; }
|
|
||||||
public void setAveragePeriodLength(Integer averagePeriodLength) { this.averagePeriodLength = averagePeriodLength; }
|
|
||||||
public OffsetDateTime getUpdatedAt() { return updatedAt; }
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package org.zaine.app.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
|
||||||
import jakarta.persistence.GenerationType;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.PrePersist;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "pc_support_messages")
|
|
||||||
public class PeriodSupportMessage {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(name = "author_name")
|
|
||||||
private String authorName;
|
|
||||||
|
|
||||||
@Column(name = "message", nullable = false, columnDefinition = "text")
|
|
||||||
private String message;
|
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false, updatable = false)
|
|
||||||
private OffsetDateTime createdAt;
|
|
||||||
|
|
||||||
@PrePersist
|
|
||||||
void onInsert() {
|
|
||||||
this.createdAt = OffsetDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getId() { return id; }
|
|
||||||
public String getAuthorName() { return authorName; }
|
|
||||||
public void setAuthorName(String authorName) { this.authorName = authorName; }
|
|
||||||
public String getMessage() { return message; }
|
|
||||||
public void setMessage(String message) { this.message = message; }
|
|
||||||
public OffsetDateTime getCreatedAt() { return createdAt; }
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package org.zaine.app.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
|
||||||
import jakarta.persistence.GenerationType;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "pc_symptoms")
|
|
||||||
public class PeriodSymptom {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(nullable = false, unique = true)
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
public Long getId() { return id; }
|
|
||||||
public String getName() { return name; }
|
|
||||||
public void setName(String name) { this.name = name; }
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package org.zaine.app.notes.adapter.in.web;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.zaine.app.notes.application.port.in.NotesUseCase;
|
||||||
|
import org.zaine.app.notes.domain.Note;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/notes")
|
||||||
|
@Tag(name = "Notes", description = "Operations related to notes")
|
||||||
|
public class NotesController {
|
||||||
|
private final NotesUseCase notes;
|
||||||
|
|
||||||
|
public NotesController(NotesUseCase notes) {
|
||||||
|
this.notes = notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Get all notes")
|
||||||
|
@GetMapping
|
||||||
|
public List<NoteResponse> getAllNotes() {
|
||||||
|
return notes.getAll().stream().map(NoteResponse::from).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Create a note object")
|
||||||
|
@PostMapping(consumes = "application/json")
|
||||||
|
public NoteResponse createNote(@RequestBody CreateNoteRequest request) {
|
||||||
|
return NoteResponse.from(notes.create(request.content(), request.authorName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CreateNoteRequest(@JsonProperty("author_name") String authorName, String content) {}
|
||||||
|
|
||||||
|
public record NoteResponse(
|
||||||
|
UUID id,
|
||||||
|
String content,
|
||||||
|
@JsonProperty("author_name") String authorName,
|
||||||
|
@JsonProperty("created_at") Instant createdAt) {
|
||||||
|
static NoteResponse from(Note note) {
|
||||||
|
return new NoteResponse(note.id(), note.content(), note.authorName(), note.createdAt());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package org.zaine.app.notes.adapter.out.persistence;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "public_notes")
|
||||||
|
class NoteJpaEntity {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Column(name = "content")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@Column(name = "author_name")
|
||||||
|
private String authorName;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private Instant createdAt;
|
||||||
|
|
||||||
|
protected NoteJpaEntity() {}
|
||||||
|
|
||||||
|
NoteJpaEntity(String content, String authorName) {
|
||||||
|
this.content = content;
|
||||||
|
this.authorName = authorName;
|
||||||
|
}
|
||||||
|
|
||||||
|
UUID id() { return id; }
|
||||||
|
String content() { return content; }
|
||||||
|
String authorName() { return authorName; }
|
||||||
|
Instant createdAt() { return createdAt; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.zaine.app.notes.adapter.out.persistence;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.zaine.app.notes.application.port.out.NoteRepositoryPort;
|
||||||
|
import org.zaine.app.notes.domain.Note;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class NotePersistenceAdapter implements NoteRepositoryPort {
|
||||||
|
private final SpringDataNoteRepository repository;
|
||||||
|
|
||||||
|
NotePersistenceAdapter(SpringDataNoteRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Note> findAll() {
|
||||||
|
return repository.findAll().stream().map(NotePersistenceAdapter::toDomain).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Note save(Note note) {
|
||||||
|
return toDomain(repository.save(new NoteJpaEntity(note.content(), note.authorName())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Note toDomain(NoteJpaEntity entity) {
|
||||||
|
return new Note(entity.id(), entity.content(), entity.authorName(), entity.createdAt());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package org.zaine.app.notes.adapter.out.persistence;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
interface SpringDataNoteRepository extends JpaRepository<NoteJpaEntity, UUID> {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package org.zaine.app.notes.application.port.in;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.zaine.app.notes.domain.Note;
|
||||||
|
|
||||||
|
public interface NotesUseCase {
|
||||||
|
List<Note> getAll();
|
||||||
|
Note create(String content, String authorName);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package org.zaine.app.notes.application.port.out;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.zaine.app.notes.domain.Note;
|
||||||
|
|
||||||
|
public interface NoteRepositoryPort {
|
||||||
|
List<Note> findAll();
|
||||||
|
Note save(Note note);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package org.zaine.app.notes.application.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.zaine.app.notes.application.port.in.NotesUseCase;
|
||||||
|
import org.zaine.app.notes.application.port.out.NoteRepositoryPort;
|
||||||
|
import org.zaine.app.notes.domain.Note;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class NotesApplicationService implements NotesUseCase {
|
||||||
|
private final NoteRepositoryPort repository;
|
||||||
|
|
||||||
|
public NotesApplicationService(NoteRepositoryPort repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Note> getAll() {
|
||||||
|
return repository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Note create(String content, String authorName) {
|
||||||
|
return repository.save(Note.create(content, authorName));
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/main/java/org/zaine/app/notes/domain/Note.java
Normal file
10
src/main/java/org/zaine/app/notes/domain/Note.java
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package org.zaine.app.notes.domain;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record Note(UUID id, String content, String authorName, Instant createdAt) {
|
||||||
|
public static Note create(String content, String authorName) {
|
||||||
|
return new Note(null, content, authorName, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package org.zaine.app.repositories;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.zaine.app.model.DailyCheckin;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public interface DailyCheckinRepository extends JpaRepository<DailyCheckin, Long> {
|
|
||||||
Optional<DailyCheckin> findByDate(LocalDate date);
|
|
||||||
List<DailyCheckin> findByDateBetweenOrderByDateDesc(LocalDate from, LocalDate to);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package org.zaine.app.repositories;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import java.util.UUID;
|
|
||||||
import org.zaine.app.model.Notes;
|
|
||||||
|
|
||||||
public interface NotesRepository extends JpaRepository<Notes, UUID> {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package org.zaine.app.repositories;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.zaine.app.model.PeriodCycle;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public interface PeriodCycleRepository extends JpaRepository<PeriodCycle, Long> {
|
|
||||||
Optional<PeriodCycle> findByStartDate(LocalDate startDate);
|
|
||||||
Optional<PeriodCycle> findFirstByOrderByStartDateDesc();
|
|
||||||
Optional<PeriodCycle> findFirstByStartDateLessThanOrderByStartDateDesc(LocalDate startDate);
|
|
||||||
List<PeriodCycle> findByStartDateBetweenOrderByStartDateAsc(LocalDate from, LocalDate to);
|
|
||||||
List<PeriodCycle> findTop12ByEndDateIsNotNullOrderByStartDateDesc();
|
|
||||||
List<PeriodCycle> findAllByOrderByStartDateAsc();
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package org.zaine.app.repositories;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.zaine.app.model.PeriodDailyLog;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public interface PeriodDailyLogRepository extends JpaRepository<PeriodDailyLog, Long> {
|
|
||||||
Optional<PeriodDailyLog> findByDate(LocalDate date);
|
|
||||||
List<PeriodDailyLog> findByDateBetweenOrderByDateAsc(LocalDate from, LocalDate to);
|
|
||||||
List<PeriodDailyLog> findByDateBetweenOrderByDateDesc(LocalDate from, LocalDate to);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package org.zaine.app.repositories;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.zaine.app.model.PeriodDailyReflection;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public interface PeriodDailyReflectionRepository extends JpaRepository<PeriodDailyReflection, Long> {
|
|
||||||
Optional<PeriodDailyReflection> findByDate(LocalDate date);
|
|
||||||
List<PeriodDailyReflection> findTop14ByOrderByDateDesc();
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package org.zaine.app.repositories;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.zaine.app.model.PeriodFastingLog;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public interface PeriodFastingLogRepository extends JpaRepository<PeriodFastingLog, Long> {
|
|
||||||
Optional<PeriodFastingLog> findByFastDate(LocalDate fastDate);
|
|
||||||
List<PeriodFastingLog> findTop50ByOrderByFastDateDesc();
|
|
||||||
long countByStatus(String status);
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package org.zaine.app.repositories;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.zaine.app.model.PeriodPredictionSnapshot;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public interface PeriodPredictionSnapshotRepository extends JpaRepository<PeriodPredictionSnapshot, Long> {
|
|
||||||
Optional<PeriodPredictionSnapshot> findFirstByOrderByGeneratedAtDesc();
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package org.zaine.app.repositories;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.zaine.app.model.PeriodSettings;
|
|
||||||
|
|
||||||
public interface PeriodSettingsRepository extends JpaRepository<PeriodSettings, Short> {
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package org.zaine.app.repositories;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.zaine.app.model.PeriodSupportMessage;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public interface PeriodSupportMessageRepository extends JpaRepository<PeriodSupportMessage, Long> {
|
|
||||||
List<PeriodSupportMessage> findTop20ByOrderByCreatedAtDesc();
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package org.zaine.app.repositories;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.zaine.app.model.PeriodSymptom;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public interface PeriodSymptomRepository extends JpaRepository<PeriodSymptom, Long> {
|
|
||||||
Optional<PeriodSymptom> findByNameIgnoreCase(String name);
|
|
||||||
List<PeriodSymptom> findAllByOrderByNameAsc();
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
package org.zaine.app.security;
|
|
||||||
|
|
||||||
import jakarta.servlet.FilterChain;
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
|
||||||
import org.springframework.web.method.HandlerMethod;
|
|
||||||
import org.springframework.web.servlet.HandlerExecutionChain;
|
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class JwtAuthFilter extends OncePerRequestFilter {
|
|
||||||
|
|
||||||
private final JwtUtil jwtUtil;
|
|
||||||
private final RequestMappingHandlerMapping requestMappingHandlerMapping;
|
|
||||||
|
|
||||||
public JwtAuthFilter(JwtUtil jwtUtil, RequestMappingHandlerMapping requestMappingHandlerMapping) {
|
|
||||||
this.jwtUtil = jwtUtil;
|
|
||||||
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doFilterInternal(HttpServletRequest request,
|
|
||||||
HttpServletResponse response,
|
|
||||||
FilterChain filterChain) throws ServletException, IOException {
|
|
||||||
if (!endpointRequiresAuth(request)) {
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String authHeader = request.getHeader("Authorization");
|
|
||||||
|
|
||||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
|
||||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing or invalid Authorization header");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String token = authHeader.substring(7);
|
|
||||||
|
|
||||||
if (!jwtUtil.isTokenValid(token)) {
|
|
||||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid or expired token");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String username = jwtUtil.extractUsername(token);
|
|
||||||
var auth = new UsernamePasswordAuthenticationToken(username, null, List.of());
|
|
||||||
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
|
||||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
|
||||||
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean endpointRequiresAuth(HttpServletRequest request) {
|
|
||||||
try {
|
|
||||||
HandlerExecutionChain chain = requestMappingHandlerMapping.getHandler(request);
|
|
||||||
if (chain == null) return false;
|
|
||||||
Object handler = chain.getHandler();
|
|
||||||
if (!(handler instanceof HandlerMethod method)) return false;
|
|
||||||
|
|
||||||
return method.hasMethodAnnotation(RequiresAuth.class)
|
|
||||||
|| method.getBeanType().isAnnotationPresent(RequiresAuth.class);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,11 +11,15 @@ import java.util.Date;
|
|||||||
@Component
|
@Component
|
||||||
public class JwtUtil {
|
public class JwtUtil {
|
||||||
|
|
||||||
@Value("${jwt.secret}")
|
private final String secret;
|
||||||
private String secret;
|
private final long expirationMs;
|
||||||
|
|
||||||
@Value("${jwt.expiration-ms:86400000}")
|
public JwtUtil(
|
||||||
private long expirationMs;
|
@Value("${jwt.secret}") String secret,
|
||||||
|
@Value("${jwt.expiration-ms:86400000}") long expirationMs) {
|
||||||
|
this.secret = secret;
|
||||||
|
this.expirationMs = expirationMs;
|
||||||
|
}
|
||||||
|
|
||||||
private Key getSigningKey() {
|
private Key getSigningKey() {
|
||||||
return Keys.hmacShaKeyFor(secret.getBytes());
|
return Keys.hmacShaKeyFor(secret.getBytes());
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package org.zaine.app.security;
|
|
||||||
|
|
||||||
import java.lang.annotation.*;
|
|
||||||
|
|
||||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@Documented
|
|
||||||
public @interface RequiresAuth {
|
|
||||||
}
|
|
||||||
@@ -13,17 +13,14 @@ import org.springframework.security.config.http.SessionCreationPolicy;
|
|||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
private final JwtAuthFilter jwtAuthFilter;
|
|
||||||
private final UserDetailsServiceImpl userDetailsService;
|
private final UserDetailsServiceImpl userDetailsService;
|
||||||
|
|
||||||
public SecurityConfig(JwtAuthFilter jwtAuthFilter, UserDetailsServiceImpl userDetailsService) {
|
public SecurityConfig(UserDetailsServiceImpl userDetailsService) {
|
||||||
this.jwtAuthFilter = jwtAuthFilter;
|
|
||||||
this.userDetailsService = userDetailsService;
|
this.userDetailsService = userDetailsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,8 +37,7 @@ public class SecurityConfig {
|
|||||||
"/swagger-ui.html"
|
"/swagger-ui.html"
|
||||||
).permitAll()
|
).permitAll()
|
||||||
.anyRequest().permitAll()
|
.anyRequest().permitAll()
|
||||||
)
|
);
|
||||||
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
|
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package org.zaine.app.service;
|
package org.zaine.app.service;
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.zaine.app.common.application.ApplicationException;
|
||||||
import org.zaine.app.dto.CalendarEventDTO;
|
import org.zaine.app.dto.CalendarEventDTO;
|
||||||
import org.zaine.app.dto.CreateCalendarEventDTO;
|
import org.zaine.app.dto.CreateCalendarEventDTO;
|
||||||
import org.zaine.app.model.CalendarEvent;
|
import org.zaine.app.model.CalendarEvent;
|
||||||
@@ -36,7 +35,7 @@ public class CalendarService {
|
|||||||
|
|
||||||
public List<CalendarEventDTO> getEventsForRange(LocalDate from, LocalDate to) {
|
public List<CalendarEventDTO> getEventsForRange(LocalDate from, LocalDate to) {
|
||||||
if (to.isBefore(from)) {
|
if (to.isBefore(from)) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "to must be on or after from");
|
throw ApplicationException.badRequest("to must be on or after from");
|
||||||
}
|
}
|
||||||
|
|
||||||
ZoneId zone = ZoneId.systemDefault();
|
ZoneId zone = ZoneId.systemDefault();
|
||||||
@@ -52,19 +51,19 @@ public class CalendarService {
|
|||||||
|
|
||||||
public CalendarEventDTO getEvent(Long id) {
|
public CalendarEventDTO getEvent(Long id) {
|
||||||
CalendarEvent event = calendarEventRepository.findById(id)
|
CalendarEvent event = calendarEventRepository.findById(id)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Calendar event not found"));
|
.orElseThrow(() -> ApplicationException.notFound("Calendar event not found"));
|
||||||
return toDto(event);
|
return toDto(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CalendarEventDTO createEvent(CreateCalendarEventDTO dto) {
|
public CalendarEventDTO createEvent(CreateCalendarEventDTO dto) {
|
||||||
if (dto.title() == null || dto.title().isBlank()) {
|
if (dto.title() == null || dto.title().isBlank()) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "title is required");
|
throw ApplicationException.badRequest("title is required");
|
||||||
}
|
}
|
||||||
if (dto.startsAt() == null || dto.endsAt() == null) {
|
if (dto.startsAt() == null || dto.endsAt() == null) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "startsAt and endsAt are required");
|
throw ApplicationException.badRequest("startsAt and endsAt are required");
|
||||||
}
|
}
|
||||||
if (!dto.endsAt().isAfter(dto.startsAt())) {
|
if (!dto.endsAt().isAfter(dto.startsAt())) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "endsAt must be after startsAt");
|
throw ApplicationException.badRequest("endsAt must be after startsAt");
|
||||||
}
|
}
|
||||||
|
|
||||||
CalendarEvent event = new CalendarEvent();
|
CalendarEvent event = new CalendarEvent();
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import net.fortuna.ical4j.model.Property;
|
|||||||
import net.fortuna.ical4j.model.component.CalendarComponent;
|
import net.fortuna.ical4j.model.component.CalendarComponent;
|
||||||
import net.fortuna.ical4j.model.component.VEvent;
|
import net.fortuna.ical4j.model.component.VEvent;
|
||||||
import net.fortuna.ical4j.model.property.DateProperty;
|
import net.fortuna.ical4j.model.property.DateProperty;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -40,13 +42,11 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class CalendarSyncService {
|
public class CalendarSyncService {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(CalendarSyncService.class.getName());
|
private static final Logger log = LoggerFactory.getLogger(CalendarSyncService.class);
|
||||||
|
|
||||||
private final CalendarEventRepository calendarEventRepository;
|
private final CalendarEventRepository calendarEventRepository;
|
||||||
private final CalendarSyncProperties properties;
|
private final CalendarSyncProperties properties;
|
||||||
@@ -68,9 +68,9 @@ public class CalendarSyncService {
|
|||||||
public void scheduledSync() {
|
public void scheduledSync() {
|
||||||
try {
|
try {
|
||||||
CalendarSyncResultDTO result = sync();
|
CalendarSyncResultDTO result = sync();
|
||||||
logger.info("Calendar sync complete: " + result.eventCount() + " events from " + result.sourceCount() + " sources");
|
log.info("Calendar sync complete: eventCount={}, sourceCount={}", result.eventCount(), result.sourceCount());
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
logger.log(Level.WARNING, "Calendar sync failed", ex);
|
log.warn("Scheduled calendar sync failed: {}", ex.getMessage(), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ public class CalendarSyncService {
|
|||||||
syncedSources.add(source.name());
|
syncedSources.add(source.name());
|
||||||
eventCount += events.size();
|
eventCount += events.size();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
logger.log(Level.WARNING, "Failed to sync calendar source " + source.name(), ex);
|
log.warn("Failed to sync calendar source {}: {}", source.name(), ex.getMessage(), ex);
|
||||||
errors.add(source.name() + ": " + ex.getMessage());
|
errors.add(source.name() + ": " + ex.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,7 +171,7 @@ public class CalendarSyncService {
|
|||||||
for (CalendarEvent event : events) {
|
for (CalendarEvent event : events) {
|
||||||
CalendarEvent duplicate = uniqueEvents.putIfAbsent(event.getExternalId(), event);
|
CalendarEvent duplicate = uniqueEvents.putIfAbsent(event.getExternalId(), event);
|
||||||
if (duplicate != null) {
|
if (duplicate != null) {
|
||||||
logger.info("Skipping duplicate calendar event from " + event.getSource() + " at " + event.getStartsAt());
|
log.debug("Skipping duplicate calendar event from source {} at {}", event.getSource(), event.getStartsAt());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return List.copyOf(uniqueEvents.values());
|
return List.copyOf(uniqueEvents.values());
|
||||||
@@ -208,7 +208,7 @@ public class CalendarSyncService {
|
|||||||
try {
|
try {
|
||||||
periods = event.calculateRecurrenceSet(syncWindow);
|
periods = event.calculateRecurrenceSet(syncWindow);
|
||||||
} catch (RuntimeException ex) {
|
} catch (RuntimeException ex) {
|
||||||
logger.log(Level.WARNING, "Failed to expand recurrence for " + uid + "; using original event only", ex);
|
log.warn("Failed to expand recurrence for event {}; using original event only: {}", uid, ex.getMessage());
|
||||||
periods = Set.of();
|
periods = Set.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
package org.zaine.app.service;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.zaine.app.dto.CalibreBookDTO;
|
|
||||||
|
|
||||||
import java.sql.DriverManager;
|
|
||||||
import java.sql.ResultSet;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads book metadata directly from the Calibre SQLite database.
|
|
||||||
*
|
|
||||||
* The path is injected from application.properties:
|
|
||||||
* calibre.db.path=/home/zaine/master-folder/projects/calibre/library/metadata.db
|
|
||||||
*
|
|
||||||
* We open a fresh connection per call — the Calibre DB is local, small,
|
|
||||||
* and this service is called infrequently (page load only), so connection
|
|
||||||
* pooling is not worth the complexity here.
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
public class CalibreService {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(CalibreService.class);
|
|
||||||
|
|
||||||
@Value("${calibre.db.path}")
|
|
||||||
private String calibreDbPath;
|
|
||||||
|
|
||||||
private static final String QUERY = """
|
|
||||||
SELECT
|
|
||||||
b.id AS book_id,
|
|
||||||
b.title AS title,
|
|
||||||
GROUP_CONCAT(a.name, ', ') AS authors
|
|
||||||
FROM books b
|
|
||||||
LEFT JOIN books_authors_link bal ON b.id = bal.book
|
|
||||||
LEFT JOIN authors a ON bal.author = a.id
|
|
||||||
GROUP BY b.id
|
|
||||||
ORDER BY b.title COLLATE NOCASE
|
|
||||||
""";
|
|
||||||
|
|
||||||
public List<CalibreBookDTO> getAllBooks() {
|
|
||||||
List<CalibreBookDTO> books = new ArrayList<>();
|
|
||||||
String jdbcUrl = "jdbc:sqlite:" + calibreDbPath;
|
|
||||||
|
|
||||||
try (var conn = DriverManager.getConnection(jdbcUrl);
|
|
||||||
var stmt = conn.createStatement();
|
|
||||||
ResultSet rs = stmt.executeQuery(QUERY)) {
|
|
||||||
|
|
||||||
while (rs.next()) {
|
|
||||||
books.add(new CalibreBookDTO(
|
|
||||||
rs.getLong("book_id"),
|
|
||||||
rs.getString("title"),
|
|
||||||
rs.getString("authors")
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to read Calibre database at {}: {}", calibreDbPath, e.getMessage(), e);
|
|
||||||
// Return empty list rather than blowing up the page —
|
|
||||||
// the book picker will simply show nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
return books;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
package org.zaine.app.service;
|
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
|
||||||
import org.zaine.app.dto.EveningCheckinRequest;
|
|
||||||
import org.zaine.app.dto.MorningCheckinRequest;
|
|
||||||
import org.zaine.app.dto.TodayStatusDTO;
|
|
||||||
import org.zaine.app.model.DailyCheckin;
|
|
||||||
import org.zaine.app.repositories.DailyCheckinRepository;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class CheckinService {
|
|
||||||
|
|
||||||
private final DailyCheckinRepository dailyCheckinRepository;
|
|
||||||
|
|
||||||
public CheckinService(DailyCheckinRepository dailyCheckinRepository) {
|
|
||||||
this.dailyCheckinRepository = dailyCheckinRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TodayStatusDTO getTodayStatus() {
|
|
||||||
return toStatus(getOrCreate(LocalDate.now()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<TodayStatusDTO> getHistory(LocalDate from, LocalDate to) {
|
|
||||||
if (to.isBefore(from)) {
|
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "to must be on or after from");
|
|
||||||
}
|
|
||||||
return dailyCheckinRepository.findByDateBetweenOrderByDateDesc(from, to)
|
|
||||||
.stream()
|
|
||||||
.map(this::toStatus)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public TodayStatusDTO saveMorning(MorningCheckinRequest request) {
|
|
||||||
LocalDate date = request.date() == null ? LocalDate.now() : request.date();
|
|
||||||
validateSleepHours(request.sleepHours());
|
|
||||||
|
|
||||||
DailyCheckin checkin = getOrCreate(date);
|
|
||||||
checkin.setSleepHours(request.sleepHours());
|
|
||||||
checkin.setMorningEnergyLevel(request.energyLevel());
|
|
||||||
checkin.setMorningMoodLevel(request.mood());
|
|
||||||
checkin.setFajr(request.fajr());
|
|
||||||
checkin.setQuran(request.quran());
|
|
||||||
checkin.setExercise(request.exercise());
|
|
||||||
checkin.setMorningNote(blankToNull(request.note()));
|
|
||||||
checkin.setMorningCompletedAt(OffsetDateTime.now());
|
|
||||||
|
|
||||||
return toStatus(dailyCheckinRepository.save(checkin));
|
|
||||||
}
|
|
||||||
|
|
||||||
public TodayStatusDTO saveEvening(EveningCheckinRequest request) {
|
|
||||||
LocalDate date = request.date() == null ? LocalDate.now() : request.date();
|
|
||||||
|
|
||||||
DailyCheckin checkin = getOrCreate(date);
|
|
||||||
checkin.setEveningMoodLevel(request.mood());
|
|
||||||
checkin.setStressLevel(request.stressLevel());
|
|
||||||
checkin.setDuties(request.duties());
|
|
||||||
checkin.setZikr(request.zikr());
|
|
||||||
checkin.setSalah(request.salah());
|
|
||||||
checkin.setReflection(blankToNull(request.reflection()));
|
|
||||||
checkin.setBestThingToday(blankToNull(request.bestThingToday()));
|
|
||||||
checkin.setEveningCompletedAt(OffsetDateTime.now());
|
|
||||||
|
|
||||||
return toStatus(dailyCheckinRepository.save(checkin));
|
|
||||||
}
|
|
||||||
|
|
||||||
private DailyCheckin getOrCreate(LocalDate date) {
|
|
||||||
return dailyCheckinRepository.findByDate(date).orElseGet(() -> {
|
|
||||||
DailyCheckin checkin = new DailyCheckin();
|
|
||||||
checkin.setDate(date);
|
|
||||||
return checkin;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private TodayStatusDTO toStatus(DailyCheckin checkin) {
|
|
||||||
return new TodayStatusDTO(
|
|
||||||
checkin.getDate(),
|
|
||||||
checkin.getMorningCompletedAt() != null,
|
|
||||||
checkin.getEveningCompletedAt() != null,
|
|
||||||
checkin.getSleepHours(),
|
|
||||||
checkin.getMorningEnergyLevel(),
|
|
||||||
checkin.getMorningMoodLevel(),
|
|
||||||
checkin.getEveningMoodLevel(),
|
|
||||||
checkin.getStressLevel(),
|
|
||||||
checkin.getFajr(),
|
|
||||||
checkin.getQuran(),
|
|
||||||
checkin.getExercise(),
|
|
||||||
checkin.getDuties(),
|
|
||||||
checkin.getZikr(),
|
|
||||||
checkin.getSalah(),
|
|
||||||
checkin.getReflection(),
|
|
||||||
checkin.getMorningCompletedAt(),
|
|
||||||
checkin.getEveningCompletedAt());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateSleepHours(BigDecimal sleepHours) {
|
|
||||||
if (sleepHours == null || sleepHours.compareTo(BigDecimal.ZERO) < 0 || sleepHours.compareTo(new BigDecimal("24")) > 0) {
|
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "sleepHours must be between 0 and 24");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String blankToNull(String value) {
|
|
||||||
return value == null || value.isBlank() ? null : value.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
package org.zaine.app.service;
|
package org.zaine.app.service;
|
||||||
|
|
||||||
import java.lang.System.Logger;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.zaine.app.model.Comments;
|
import org.zaine.app.model.Comments;
|
||||||
import org.zaine.app.repositories.CommentsRepository;
|
import org.zaine.app.repositories.CommentsRepository;
|
||||||
|
import org.zaine.app.common.application.ApplicationException;
|
||||||
import java.util.stream.*;
|
import java.util.stream.*;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class CommentsService {
|
public class CommentsService {
|
||||||
private static final Logger logger = System.getLogger(CommentsService.class.getName());
|
private static final Logger log = LoggerFactory.getLogger(CommentsService.class);
|
||||||
|
|
||||||
private final CommentsRepository commentsRepository;
|
private final CommentsRepository commentsRepository;
|
||||||
|
|
||||||
@@ -22,7 +25,7 @@ public class CommentsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<Comments> getAllCommentsBySlug(String page_slug) {
|
public List<Comments> getAllCommentsBySlug(String page_slug) {
|
||||||
logger.log(System.Logger.Level.INFO, "Entering getAllCommentsBySlug with slug: " + page_slug);
|
log.debug("Finding comments for page slug {}", page_slug);
|
||||||
List<Comments> allComments = commentsRepository.findAll();
|
List<Comments> allComments = commentsRepository.findAll();
|
||||||
List<Comments> filteredComments = allComments.stream()
|
List<Comments> filteredComments = allComments.stream()
|
||||||
.filter(comment -> page_slug.equals(comment.getPageSlug()))
|
.filter(comment -> page_slug.equals(comment.getPageSlug()))
|
||||||
@@ -31,28 +34,32 @@ public class CommentsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Comments getCommentById(Integer id) {
|
public Comments getCommentById(Integer id) {
|
||||||
logger.log(System.Logger.Level.INFO, "Entering getCommentById with id: " + id);
|
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
logger.log(System.Logger.Level.WARNING, "Comment id is null. Operation aborted.");
|
log.warn("Comment lookup skipped because id was null");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return commentsRepository.findById(id).get();
|
return commentsRepository.findById(id)
|
||||||
|
.orElseThrow(() -> ApplicationException.notFound("Comment not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addComment(Comments comment) {
|
public Comments addComment(String pageSlug, String author, String content, Integer parentId) {
|
||||||
if (comment == null) {
|
if (content == null || content.trim().isEmpty()) {
|
||||||
logger.log(System.Logger.Level.WARNING, "Comment is null. Operation aborted.");
|
log.warn("Comment creation skipped because content was empty");
|
||||||
return;
|
return null;
|
||||||
} else {
|
|
||||||
logger.log(System.Logger.Level.INFO, "Adding comment with id: " + comment.getId());
|
|
||||||
commentsRepository.save(comment);
|
|
||||||
logger.log(System.Logger.Level.INFO, "Added new comment with id: " + comment.getId());
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Comments comment = new Comments();
|
||||||
|
comment.setPageSlug(pageSlug);
|
||||||
|
comment.setAuthor(author);
|
||||||
|
comment.setContent(content);
|
||||||
|
comment.setParentId(parentId);
|
||||||
|
comment.setCreatedAt(Instant.now());
|
||||||
|
Comments saved = commentsRepository.save(comment);
|
||||||
|
log.info("Created comment id={} for pageSlug={}", saved.getId(), saved.getPageSlug());
|
||||||
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Comments> getCommentThread(Integer comment_id) {
|
public List<Comments> getCommentThread(Integer comment_id) {
|
||||||
logger.log(System.Logger.Level.INFO, "Entering getCommentThread with id: " + comment_id);
|
log.debug("Finding comment thread for id {}", comment_id);
|
||||||
return commentsRepository.findCommentThread(comment_id);
|
return commentsRepository.findCommentThread(comment_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
package org.zaine.app.service;
|
package org.zaine.app.service;
|
||||||
import java.lang.System.Logger;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.zaine.app.model.Competencies;
|
import org.zaine.app.model.Competencies;
|
||||||
import org.zaine.app.repositories.CompetenciesRepository;
|
import org.zaine.app.repositories.CompetenciesRepository;
|
||||||
|
import org.zaine.app.common.application.ApplicationException;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class CompetenciesService {
|
public class CompetenciesService {
|
||||||
private static final Logger logger = System.getLogger(CompetenciesService.class.getName());
|
private static final Logger log = LoggerFactory.getLogger(CompetenciesService.class);
|
||||||
private final CompetenciesRepository competenciesRepository;
|
private final CompetenciesRepository competenciesRepository;
|
||||||
private static final List<String> ALLOWED_STATES =
|
private static final List<String> ALLOWED_STATES =
|
||||||
List.of("completed", "manager_review", "in_progress", "not_started", "comments");
|
List.of("completed", "manager_review", "in_progress", "not_started", "comments");
|
||||||
@@ -25,12 +27,12 @@ public class CompetenciesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Competencies getCompetencyById(Integer id) {
|
public Competencies getCompetencyById(Integer id) {
|
||||||
logger.log(System.Logger.Level.INFO, "Entering getCompetencyById with id: " + id);
|
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
logger.log(System.Logger.Level.WARNING, "Competency id is null. Operation aborted.");
|
log.warn("Competency lookup skipped because id was null");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return competenciesRepository.findById(id).get();
|
return competenciesRepository.findById(id)
|
||||||
|
.orElseThrow(() -> ApplicationException.notFound("Competency not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateCompetencyState(Integer id, String newState) {
|
public void updateCompetencyState(Integer id, String newState) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.zaine.app.service;
|
package org.zaine.app.service;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.zaine.app.dto.MotalahSessionDTO;
|
import org.zaine.app.dto.MotalahSessionDTO;
|
||||||
import org.zaine.app.model.MotalahSession;
|
import org.zaine.app.model.MotalahSession;
|
||||||
@@ -13,8 +12,11 @@ import java.util.List;
|
|||||||
@Service
|
@Service
|
||||||
public class MotalahService {
|
public class MotalahService {
|
||||||
|
|
||||||
@Autowired
|
private final MotalahSessionRepository repo;
|
||||||
private MotalahSessionRepository repo;
|
|
||||||
|
public MotalahService(MotalahSessionRepository repo) {
|
||||||
|
this.repo = repo;
|
||||||
|
}
|
||||||
|
|
||||||
/** All sessions, newest first. */
|
/** All sessions, newest first. */
|
||||||
public List<MotalahSession> getAll() {
|
public List<MotalahSession> getAll() {
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
package org.zaine.app.service;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.zaine.app.model.Notes;
|
|
||||||
import org.zaine.app.repositories.NotesRepository;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class NotesService {
|
|
||||||
private final NotesRepository notesRepository;
|
|
||||||
|
|
||||||
public NotesService(NotesRepository notesRepository) {
|
|
||||||
this.notesRepository = notesRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Notes> getAllNotes() {
|
|
||||||
return Optional.ofNullable(notesRepository.findAll())
|
|
||||||
.orElseThrow(() -> new RuntimeException("Failed to retrieve notes"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Notes createNote(Notes note) {
|
|
||||||
return Optional.ofNullable(notesRepository.save(note)).orElseThrow(() -> new RuntimeException("Failed to create note"));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,571 +0,0 @@
|
|||||||
package org.zaine.app.service;
|
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.CalendarDayDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.CreateSymptomRequest;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.CycleTrendDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.DailyLogDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.DailyLogRequest;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.DashboardDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.FastingLogDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.FastingLogRequest;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.InsightsDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.MoodTrendDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.PeriodCycleDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.PeriodCycleRequest;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.PeriodEntryRequest;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.PredictionDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.ReflectionDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.ReflectionRequest;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SettingsDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SettingsRequest;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SupportMessageDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SupportMessageRequest;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SymptomDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SymptomTrendDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.WellbeingDTO;
|
|
||||||
import org.zaine.app.model.PeriodCycle;
|
|
||||||
import org.zaine.app.model.PeriodDailyReflection;
|
|
||||||
import org.zaine.app.model.PeriodDailyLog;
|
|
||||||
import org.zaine.app.model.PeriodFastingLog;
|
|
||||||
import org.zaine.app.model.PeriodPredictionSnapshot;
|
|
||||||
import org.zaine.app.model.PeriodSettings;
|
|
||||||
import org.zaine.app.model.PeriodSupportMessage;
|
|
||||||
import org.zaine.app.model.PeriodSymptom;
|
|
||||||
import org.zaine.app.repositories.PeriodCycleRepository;
|
|
||||||
import org.zaine.app.repositories.PeriodDailyReflectionRepository;
|
|
||||||
import org.zaine.app.repositories.PeriodDailyLogRepository;
|
|
||||||
import org.zaine.app.repositories.PeriodFastingLogRepository;
|
|
||||||
import org.zaine.app.repositories.PeriodPredictionSnapshotRepository;
|
|
||||||
import org.zaine.app.repositories.PeriodSettingsRepository;
|
|
||||||
import org.zaine.app.repositories.PeriodSupportMessageRepository;
|
|
||||||
import org.zaine.app.repositories.PeriodSymptomRepository;
|
|
||||||
import org.zaine.app.service.PeriodPredictionService.PredictionEstimate;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class PeriodCompanionService {
|
|
||||||
private final PeriodCycleRepository cycleRepository;
|
|
||||||
private final PeriodDailyLogRepository dailyLogRepository;
|
|
||||||
private final PeriodDailyReflectionRepository dailyReflectionRepository;
|
|
||||||
private final PeriodFastingLogRepository fastingLogRepository;
|
|
||||||
private final PeriodSymptomRepository symptomRepository;
|
|
||||||
private final PeriodPredictionSnapshotRepository predictionRepository;
|
|
||||||
private final PeriodSettingsRepository settingsRepository;
|
|
||||||
private final PeriodSupportMessageRepository supportMessageRepository;
|
|
||||||
private final PeriodPredictionService predictionService;
|
|
||||||
|
|
||||||
public PeriodCompanionService(
|
|
||||||
PeriodCycleRepository cycleRepository,
|
|
||||||
PeriodDailyLogRepository dailyLogRepository,
|
|
||||||
PeriodDailyReflectionRepository dailyReflectionRepository,
|
|
||||||
PeriodFastingLogRepository fastingLogRepository,
|
|
||||||
PeriodSymptomRepository symptomRepository,
|
|
||||||
PeriodPredictionSnapshotRepository predictionRepository,
|
|
||||||
PeriodSettingsRepository settingsRepository,
|
|
||||||
PeriodSupportMessageRepository supportMessageRepository,
|
|
||||||
PeriodPredictionService predictionService) {
|
|
||||||
this.cycleRepository = cycleRepository;
|
|
||||||
this.dailyLogRepository = dailyLogRepository;
|
|
||||||
this.dailyReflectionRepository = dailyReflectionRepository;
|
|
||||||
this.fastingLogRepository = fastingLogRepository;
|
|
||||||
this.symptomRepository = symptomRepository;
|
|
||||||
this.predictionRepository = predictionRepository;
|
|
||||||
this.settingsRepository = settingsRepository;
|
|
||||||
this.supportMessageRepository = supportMessageRepository;
|
|
||||||
this.predictionService = predictionService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DashboardDTO getDashboard() {
|
|
||||||
LocalDate today = LocalDate.now();
|
|
||||||
List<PeriodCycle> cycles = cycleRepository.findAllByOrderByStartDateAsc();
|
|
||||||
PeriodSettings settings = getOrCreateSettings();
|
|
||||||
PredictionDTO prediction = latestPrediction(cycles, today, settings);
|
|
||||||
DailyLogDTO todayLog = dailyLogRepository.findByDate(today).map(this::toDailyLogDto).orElse(null);
|
|
||||||
return new DashboardDTO(
|
|
||||||
predictionService.currentCycle(cycles, today, settings.getAverageCycleLength(), settings.getAveragePeriodLength()),
|
|
||||||
prediction,
|
|
||||||
todayLog,
|
|
||||||
toSettingsDto(settings),
|
|
||||||
contextualSupportMessages(prediction, today),
|
|
||||||
getSupportMessages());
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<CalendarDayDTO> getCalendar(LocalDate from, LocalDate to) {
|
|
||||||
validateRange(from, to);
|
|
||||||
List<PeriodCycle> cycles = cycleRepository.findAllByOrderByStartDateAsc();
|
|
||||||
PeriodSettings settings = getOrCreateSettings();
|
|
||||||
List<PredictionEstimate> projections = predictionService.project(
|
|
||||||
cycles,
|
|
||||||
LocalDate.now(),
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
settings.getAverageCycleLength(),
|
|
||||||
settings.getAveragePeriodLength());
|
|
||||||
Map<LocalDate, PeriodDailyLog> logs = dailyLogRepository.findByDateBetweenOrderByDateAsc(from, to)
|
|
||||||
.stream()
|
|
||||||
.collect(Collectors.toMap(PeriodDailyLog::getDate, Function.identity()));
|
|
||||||
|
|
||||||
List<CalendarDayDTO> days = new ArrayList<>();
|
|
||||||
for (LocalDate date = from; !date.isAfter(to); date = date.plusDays(1)) {
|
|
||||||
PeriodDailyLog log = logs.get(date);
|
|
||||||
LocalDate currentDate = date;
|
|
||||||
boolean periodDay = cycles.stream().anyMatch(cycle -> isPeriodDay(cycle, currentDate));
|
|
||||||
days.add(new CalendarDayDTO(
|
|
||||||
date,
|
|
||||||
periodDay,
|
|
||||||
projections.stream().anyMatch(projection -> isPredictedPeriodDay(projection, currentDate)),
|
|
||||||
projections.stream().anyMatch(projection -> isFertileWindowDay(projection, currentDate)),
|
|
||||||
projections.stream().anyMatch(projection -> currentDate.equals(projection.predictedOvulationDate())),
|
|
||||||
log == null ? List.of() : log.getSymptoms().stream().map(PeriodSymptom::getName).sorted().toList(),
|
|
||||||
log == null ? null : toDailyLogDto(log)));
|
|
||||||
}
|
|
||||||
return days;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<DailyLogDTO> getDailyLogs(LocalDate from, LocalDate to) {
|
|
||||||
validateRange(from, to);
|
|
||||||
return dailyLogRepository.findByDateBetweenOrderByDateDesc(from, to).stream().map(this::toDailyLogDto).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public DailyLogDTO getDailyLog(LocalDate date) {
|
|
||||||
return dailyLogRepository.findByDate(date).map(this::toDailyLogDto).orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public DailyLogDTO saveDailyLog(DailyLogRequest request) {
|
|
||||||
LocalDate date = request.date() == null ? LocalDate.now() : request.date();
|
|
||||||
PeriodDailyLog log = dailyLogRepository.findByDate(date).orElseGet(() -> {
|
|
||||||
PeriodDailyLog newLog = new PeriodDailyLog();
|
|
||||||
newLog.setDate(date);
|
|
||||||
return newLog;
|
|
||||||
});
|
|
||||||
log.setFlowLevel(blankToNull(request.flowLevel()));
|
|
||||||
log.setMood(blankToNull(request.mood()));
|
|
||||||
log.setEnergyLevel(blankToNull(request.energyLevel()));
|
|
||||||
log.setNotes(blankToNull(request.notes()));
|
|
||||||
log.setSymptoms(resolveSymptoms(request.symptoms()));
|
|
||||||
return toDailyLogDto(dailyLogRepository.save(log));
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<SymptomDTO> getSymptoms() {
|
|
||||||
return symptomRepository.findAllByOrderByNameAsc().stream().map(this::toSymptomDto).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SymptomDTO createSymptom(CreateSymptomRequest request) {
|
|
||||||
return toSymptomDto(resolveSymptom(requiredText(request.name(), "name")));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public PeriodCycleDTO saveCycle(PeriodCycleRequest request) {
|
|
||||||
LocalDate startDate = requiredDate(request.startDate(), "startDate");
|
|
||||||
LocalDate endDate = request.endDate();
|
|
||||||
validateCycleDates(startDate, endDate);
|
|
||||||
PeriodCycle cycle = cycleRepository.findByStartDate(startDate).orElseGet(PeriodCycle::new);
|
|
||||||
cycle.setStartDate(startDate);
|
|
||||||
cycle.setEndDate(endDate);
|
|
||||||
setDerivedCycleValues(cycle);
|
|
||||||
PeriodCycle saved = cycleRepository.save(cycle);
|
|
||||||
updatePreviousCycleLength(saved);
|
|
||||||
return predictionService.toCycleDto(saved);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<PeriodCycleDTO> getCycles() {
|
|
||||||
return cycleRepository.findAllByOrderByStartDateAsc().stream().map(predictionService::toCycleDto).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public PeriodCycleDTO savePeriodEntry(PeriodEntryRequest request) {
|
|
||||||
LocalDate startDate = requiredDate(request.startDate(), "startDate");
|
|
||||||
LocalDate endDate = request.endDate() == null ? startDate : request.endDate();
|
|
||||||
validateCycleDates(startDate, endDate);
|
|
||||||
PeriodCycleDTO cycle = saveCycle(new PeriodCycleRequest(startDate, endDate));
|
|
||||||
for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
|
|
||||||
LocalDate logDate = date;
|
|
||||||
PeriodDailyLog log = dailyLogRepository.findByDate(logDate).orElseGet(() -> {
|
|
||||||
PeriodDailyLog newLog = new PeriodDailyLog();
|
|
||||||
newLog.setDate(logDate);
|
|
||||||
return newLog;
|
|
||||||
});
|
|
||||||
log.setFlowLevel(blankToNull(request.flowLevel()) == null ? "Medium" : request.flowLevel().trim());
|
|
||||||
dailyLogRepository.save(log);
|
|
||||||
}
|
|
||||||
return cycle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PredictionDTO generatePrediction() {
|
|
||||||
List<PeriodCycle> cycles = cycleRepository.findAllByOrderByStartDateAsc();
|
|
||||||
PeriodSettings settings = getOrCreateSettings();
|
|
||||||
PredictionEstimate estimate = predictionService.estimate(
|
|
||||||
cycles,
|
|
||||||
LocalDate.now(),
|
|
||||||
settings.getAverageCycleLength(),
|
|
||||||
settings.getAveragePeriodLength());
|
|
||||||
PeriodPredictionSnapshot snapshot = new PeriodPredictionSnapshot();
|
|
||||||
snapshot.setPredictedPeriodDate(estimate.predictedPeriodDate());
|
|
||||||
snapshot.setPredictedOvulationDate(estimate.predictedOvulationDate());
|
|
||||||
snapshot.setFertileWindowStart(estimate.fertileWindowStart());
|
|
||||||
snapshot.setFertileWindowEnd(estimate.fertileWindowEnd());
|
|
||||||
return predictionService.toPredictionDto(predictionRepository.save(snapshot), estimate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public InsightsDTO getInsights() {
|
|
||||||
List<PeriodCycle> cycles = cycleRepository.findAllByOrderByStartDateAsc();
|
|
||||||
List<PeriodDailyLog> logs = dailyLogRepository.findByDateBetweenOrderByDateAsc(
|
|
||||||
LocalDate.now().minusMonths(12),
|
|
||||||
LocalDate.now().plusMonths(1));
|
|
||||||
List<CycleTrendDTO> cycleTrends = cycles.stream()
|
|
||||||
.map(cycle -> new CycleTrendDTO(cycle.getStartDate(), cycle.getCycleLength(), cycle.getPeriodLength()))
|
|
||||||
.toList();
|
|
||||||
List<SymptomTrendDTO> symptomTrends = logs.stream()
|
|
||||||
.flatMap(log -> log.getSymptoms().stream())
|
|
||||||
.collect(Collectors.groupingBy(PeriodSymptom::getName, Collectors.counting()))
|
|
||||||
.entrySet()
|
|
||||||
.stream()
|
|
||||||
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
|
|
||||||
.map(entry -> new SymptomTrendDTO(entry.getKey(), entry.getValue()))
|
|
||||||
.toList();
|
|
||||||
List<MoodTrendDTO> moodTrends = logs.stream()
|
|
||||||
.filter(log -> log.getMood() != null)
|
|
||||||
.map(log -> new MoodTrendDTO(log.getDate(), log.getMood()))
|
|
||||||
.toList();
|
|
||||||
return new InsightsDTO(cycleTrends, symptomTrends, moodTrends);
|
|
||||||
}
|
|
||||||
|
|
||||||
public WellbeingDTO getWellbeing() {
|
|
||||||
LocalDate today = LocalDate.now();
|
|
||||||
List<PeriodCycle> cycles = cycleRepository.findAllByOrderByStartDateAsc();
|
|
||||||
PeriodSettings settings = getOrCreateSettings();
|
|
||||||
String phase = predictionService.currentCycle(
|
|
||||||
cycles,
|
|
||||||
today,
|
|
||||||
settings.getAverageCycleLength(),
|
|
||||||
settings.getAveragePeriodLength()).phase();
|
|
||||||
ReflectionDTO todayReflection = dailyReflectionRepository.findByDate(today)
|
|
||||||
.map(this::toReflectionDto)
|
|
||||||
.orElseGet(() -> new ReflectionDTO(null, today, defaultReflectionPrompt(phase), null, null, null));
|
|
||||||
|
|
||||||
return new WellbeingDTO(
|
|
||||||
phase,
|
|
||||||
bodyInsight(phase),
|
|
||||||
supportiveAction(phase),
|
|
||||||
spiritualReminder(phase),
|
|
||||||
fiqhNote(phase),
|
|
||||||
todayReflection,
|
|
||||||
dailyReflectionRepository.findTop14ByOrderByDateDesc().stream().map(this::toReflectionDto).toList(),
|
|
||||||
getFastingLogs(),
|
|
||||||
fastingLogRepository.countByStatus("MISSED"),
|
|
||||||
fastingLogRepository.countByStatus("MADE_UP"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<FastingLogDTO> getFastingLogs() {
|
|
||||||
return fastingLogRepository.findTop50ByOrderByFastDateDesc().stream().map(this::toFastingLogDto).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public FastingLogDTO saveFastingLog(FastingLogRequest request) {
|
|
||||||
if (request == null) {
|
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "fasting log payload is required");
|
|
||||||
}
|
|
||||||
LocalDate fastDate = requiredDate(request.fastDate(), "fastDate");
|
|
||||||
String status = requiredText(request.status(), "status").toUpperCase(Locale.ROOT);
|
|
||||||
if (!status.equals("MISSED") && !status.equals("MADE_UP")) {
|
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "status must be MISSED or MADE_UP");
|
|
||||||
}
|
|
||||||
PeriodFastingLog log = fastingLogRepository.findByFastDate(fastDate).orElseGet(PeriodFastingLog::new);
|
|
||||||
log.setFastDate(fastDate);
|
|
||||||
log.setStatus(status);
|
|
||||||
log.setNotes(blankToNull(request.notes()));
|
|
||||||
return toFastingLogDto(fastingLogRepository.save(log));
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReflectionDTO saveReflection(ReflectionRequest request) {
|
|
||||||
if (request == null) {
|
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "reflection payload is required");
|
|
||||||
}
|
|
||||||
LocalDate date = request.date() == null ? LocalDate.now() : request.date();
|
|
||||||
String prompt = blankToNull(request.prompt());
|
|
||||||
if (prompt == null) {
|
|
||||||
List<PeriodCycle> cycles = cycleRepository.findAllByOrderByStartDateAsc();
|
|
||||||
PeriodSettings settings = getOrCreateSettings();
|
|
||||||
String phase = predictionService.currentCycle(
|
|
||||||
cycles,
|
|
||||||
date,
|
|
||||||
settings.getAverageCycleLength(),
|
|
||||||
settings.getAveragePeriodLength()).phase();
|
|
||||||
prompt = defaultReflectionPrompt(phase);
|
|
||||||
}
|
|
||||||
|
|
||||||
PeriodDailyReflection reflection = dailyReflectionRepository.findByDate(date).orElseGet(PeriodDailyReflection::new);
|
|
||||||
reflection.setDate(date);
|
|
||||||
reflection.setPrompt(prompt);
|
|
||||||
reflection.setResponse(blankToNull(request.response()));
|
|
||||||
return toReflectionDto(dailyReflectionRepository.save(reflection));
|
|
||||||
}
|
|
||||||
|
|
||||||
public SettingsDTO getSettings() {
|
|
||||||
return toSettingsDto(getOrCreateSettings());
|
|
||||||
}
|
|
||||||
|
|
||||||
public SettingsDTO updateSettings(SettingsRequest request) {
|
|
||||||
if (request == null) {
|
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "settings payload is required");
|
|
||||||
}
|
|
||||||
PeriodSettings settings = getOrCreateSettings();
|
|
||||||
if (request.averageCycleLength() != null) {
|
|
||||||
validateIntegerRange(request.averageCycleLength(), 15, 60, "averageCycleLength");
|
|
||||||
settings.setAverageCycleLength(request.averageCycleLength());
|
|
||||||
}
|
|
||||||
if (request.averagePeriodLength() != null) {
|
|
||||||
validateIntegerRange(request.averagePeriodLength(), 1, 15, "averagePeriodLength");
|
|
||||||
settings.setAveragePeriodLength(request.averagePeriodLength());
|
|
||||||
}
|
|
||||||
return toSettingsDto(settingsRepository.save(settings));
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<SupportMessageDTO> getSupportMessages() {
|
|
||||||
return supportMessageRepository.findTop20ByOrderByCreatedAtDesc().stream().map(this::toSupportMessageDto).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SupportMessageDTO createSupportMessage(SupportMessageRequest request) {
|
|
||||||
PeriodSupportMessage message = new PeriodSupportMessage();
|
|
||||||
message.setAuthorName(blankToNull(request.authorName()));
|
|
||||||
message.setMessage(requiredText(request.message(), "message"));
|
|
||||||
return toSupportMessageDto(supportMessageRepository.save(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
private PredictionDTO latestPrediction(List<PeriodCycle> cycles, LocalDate today, PeriodSettings settings) {
|
|
||||||
PredictionEstimate estimate = predictionService.estimate(
|
|
||||||
cycles,
|
|
||||||
today,
|
|
||||||
settings.getAverageCycleLength(),
|
|
||||||
settings.getAveragePeriodLength());
|
|
||||||
return predictionRepository.findFirstByOrderByGeneratedAtDesc()
|
|
||||||
.map(snapshot -> predictionService.toPredictionDto(snapshot, estimate))
|
|
||||||
.orElseGet(() -> generatePrediction());
|
|
||||||
}
|
|
||||||
|
|
||||||
private PeriodSettings getOrCreateSettings() {
|
|
||||||
return settingsRepository.findById(PeriodSettings.SINGLETON_ID).orElseGet(() -> settingsRepository.save(new PeriodSettings()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setDerivedCycleValues(PeriodCycle cycle) {
|
|
||||||
if (cycle.getEndDate() != null) {
|
|
||||||
cycle.setPeriodLength((int) ChronoUnit.DAYS.between(cycle.getStartDate(), cycle.getEndDate()) + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updatePreviousCycleLength(PeriodCycle cycle) {
|
|
||||||
cycleRepository.findFirstByStartDateLessThanOrderByStartDateDesc(cycle.getStartDate()).ifPresent(previous -> {
|
|
||||||
previous.setCycleLength((int) ChronoUnit.DAYS.between(previous.getStartDate(), cycle.getStartDate()));
|
|
||||||
cycleRepository.save(previous);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isPeriodDay(PeriodCycle cycle, LocalDate date) {
|
|
||||||
if (cycle.getEndDate() != null) {
|
|
||||||
return !date.isBefore(cycle.getStartDate()) && !date.isAfter(cycle.getEndDate());
|
|
||||||
}
|
|
||||||
int periodLength = cycle.getPeriodLength() == null ? 5 : cycle.getPeriodLength();
|
|
||||||
return !date.isBefore(cycle.getStartDate()) && date.isBefore(cycle.getStartDate().plusDays(periodLength));
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isPredictedPeriodDay(PredictionEstimate prediction, LocalDate date) {
|
|
||||||
return !date.isBefore(prediction.predictedPeriodDate())
|
|
||||||
&& date.isBefore(prediction.predictedPeriodDate().plusDays(prediction.averagePeriodLength()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isFertileWindowDay(PredictionEstimate prediction, LocalDate date) {
|
|
||||||
return !date.isBefore(prediction.fertileWindowStart()) && !date.isAfter(prediction.fertileWindowEnd());
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> contextualSupportMessages(PredictionDTO prediction, LocalDate today) {
|
|
||||||
long daysUntilPeriod = ChronoUnit.DAYS.between(today, prediction.predictedPeriodDate());
|
|
||||||
long daysUntilOvulation = ChronoUnit.DAYS.between(today, prediction.predictedOvulationDate());
|
|
||||||
List<String> messages = new ArrayList<>();
|
|
||||||
if (daysUntilPeriod >= 0 && daysUntilPeriod <= 3) {
|
|
||||||
messages.add("Period may be close. Keep things gentle and flexible.");
|
|
||||||
}
|
|
||||||
if (daysUntilOvulation >= 0 && daysUntilOvulation <= 3) {
|
|
||||||
messages.add("Ovulation is approaching. Energy and mood may shift.");
|
|
||||||
}
|
|
||||||
if (!today.isBefore(prediction.fertileWindowStart()) && !today.isAfter(prediction.fertileWindowEnd())) {
|
|
||||||
messages.add("Fertile window is active based on recent cycle history.");
|
|
||||||
}
|
|
||||||
if (messages.isEmpty()) {
|
|
||||||
messages.add("A steady day to check in, notice patterns, and support each other.");
|
|
||||||
}
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
private LinkedHashSet<PeriodSymptom> resolveSymptoms(List<String> names) {
|
|
||||||
if (names == null) {
|
|
||||||
return new LinkedHashSet<>();
|
|
||||||
}
|
|
||||||
return names.stream()
|
|
||||||
.map(name -> name == null ? "" : name.trim())
|
|
||||||
.filter(name -> !name.isBlank())
|
|
||||||
.map(this::resolveSymptom)
|
|
||||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
|
||||||
}
|
|
||||||
|
|
||||||
private PeriodSymptom resolveSymptom(String name) {
|
|
||||||
String normalized = toDisplayName(name);
|
|
||||||
return symptomRepository.findByNameIgnoreCase(normalized).orElseGet(() -> {
|
|
||||||
PeriodSymptom symptom = new PeriodSymptom();
|
|
||||||
symptom.setName(normalized);
|
|
||||||
return symptomRepository.save(symptom);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private DailyLogDTO toDailyLogDto(PeriodDailyLog log) {
|
|
||||||
return new DailyLogDTO(
|
|
||||||
log.getId(),
|
|
||||||
log.getDate(),
|
|
||||||
log.getFlowLevel(),
|
|
||||||
log.getMood(),
|
|
||||||
log.getEnergyLevel(),
|
|
||||||
log.getNotes(),
|
|
||||||
log.getSymptoms().stream().sorted(Comparator.comparing(PeriodSymptom::getName)).map(this::toSymptomDto).toList(),
|
|
||||||
log.getCreatedAt(),
|
|
||||||
log.getUpdatedAt());
|
|
||||||
}
|
|
||||||
|
|
||||||
private SymptomDTO toSymptomDto(PeriodSymptom symptom) {
|
|
||||||
return new SymptomDTO(symptom.getId(), symptom.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
private SupportMessageDTO toSupportMessageDto(PeriodSupportMessage message) {
|
|
||||||
return new SupportMessageDTO(message.getId(), message.getAuthorName(), message.getMessage(), message.getCreatedAt());
|
|
||||||
}
|
|
||||||
|
|
||||||
private FastingLogDTO toFastingLogDto(PeriodFastingLog log) {
|
|
||||||
return new FastingLogDTO(log.getId(), log.getFastDate(), log.getStatus(), log.getNotes(), log.getCreatedAt(), log.getUpdatedAt());
|
|
||||||
}
|
|
||||||
|
|
||||||
private ReflectionDTO toReflectionDto(PeriodDailyReflection reflection) {
|
|
||||||
return new ReflectionDTO(
|
|
||||||
reflection.getId(),
|
|
||||||
reflection.getDate(),
|
|
||||||
reflection.getPrompt(),
|
|
||||||
reflection.getResponse(),
|
|
||||||
reflection.getCreatedAt(),
|
|
||||||
reflection.getUpdatedAt());
|
|
||||||
}
|
|
||||||
|
|
||||||
private SettingsDTO toSettingsDto(PeriodSettings settings) {
|
|
||||||
return new SettingsDTO(settings.getAverageCycleLength(), settings.getAveragePeriodLength(), settings.getUpdatedAt());
|
|
||||||
}
|
|
||||||
|
|
||||||
private String bodyInsight(String phase) {
|
|
||||||
return switch (phase) {
|
|
||||||
case "Menstrual" -> "Energy can be lower during hayd. Rest, warmth, hydration, and lighter plans may feel better.";
|
|
||||||
case "Follicular" -> "Energy often rebuilds after bleeding. This can be a good time for planning, movement, and fresh routines.";
|
|
||||||
case "Ovulation" -> "Some people feel more social and alert around ovulation, while others notice cramps or tenderness.";
|
|
||||||
case "Luteal" -> "Mood, sleep, cravings, and focus can shift in the luteal phase. Gentler expectations can help.";
|
|
||||||
default -> "Track a few cycles and the guidance will become more specific to your rhythm.";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private String supportiveAction(String phase) {
|
|
||||||
return switch (phase) {
|
|
||||||
case "Menstrual" -> "Choose one kind thing for the body today: heat, a slow walk, extra water, or an early night.";
|
|
||||||
case "Follicular" -> "Pick one task that benefits future you while your energy is returning.";
|
|
||||||
case "Ovulation" -> "Use the clearer moments for connection, but keep space for rest if your body asks for it.";
|
|
||||||
case "Luteal" -> "Reduce friction where possible: prepare simple meals, lower the pace, and write down what feels heavy.";
|
|
||||||
default -> "Make one small note about what your body is telling you today.";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private String spiritualReminder(String phase) {
|
|
||||||
return switch (phase) {
|
|
||||||
case "Menstrual" -> "Even when salah and fasting pause, dhikr, dua, gratitude, and reflection remain open doors.";
|
|
||||||
case "Follicular" -> "As energy returns, renew intentions gently rather than trying to catch up all at once.";
|
|
||||||
case "Ovulation" -> "Use moments of ease for shukr and sincere dua.";
|
|
||||||
case "Luteal" -> "When emotions feel louder, small consistent remembrance can be grounding.";
|
|
||||||
default -> "Notice the body as an amanah and meet it with mercy.";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private String fiqhNote(String phase) {
|
|
||||||
if ("Menstrual".equals(phase)) {
|
|
||||||
return "Hayd-related rulings can vary by madhhab and situation. For spotting, istihadah, ghusl, or confusing bleeding, check a trusted scholar or teacher.";
|
|
||||||
}
|
|
||||||
return "For fasting make-ups, spotting, istihadah, and ghusl questions, use this as a reminder tool and confirm details with trusted fiqh guidance.";
|
|
||||||
}
|
|
||||||
|
|
||||||
private String defaultReflectionPrompt(String phase) {
|
|
||||||
return switch (phase) {
|
|
||||||
case "Menstrual" -> "What would mercy toward your body look like today?";
|
|
||||||
case "Follicular" -> "What intention do you want to rebuild gently this week?";
|
|
||||||
case "Ovulation" -> "Where can you use today’s energy with gratitude and balance?";
|
|
||||||
case "Luteal" -> "What can you simplify before your next period begins?";
|
|
||||||
default -> "What did you notice in your body and heart today?";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateRange(LocalDate from, LocalDate to) {
|
|
||||||
requiredDate(from, "from");
|
|
||||||
requiredDate(to, "to");
|
|
||||||
if (to.isBefore(from)) {
|
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "to must be on or after from");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateCycleDates(LocalDate startDate, LocalDate endDate) {
|
|
||||||
if (endDate != null && endDate.isBefore(startDate)) {
|
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "endDate must be on or after startDate");
|
|
||||||
}
|
|
||||||
if (endDate != null && ChronoUnit.DAYS.between(startDate, endDate) > 14) {
|
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "period length must be 15 days or less");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private LocalDate requiredDate(LocalDate value, String field) {
|
|
||||||
if (value == null) {
|
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, field + " is required");
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String requiredText(String value, String field) {
|
|
||||||
String text = blankToNull(value);
|
|
||||||
if (text == null) {
|
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, field + " is required");
|
|
||||||
}
|
|
||||||
if (text.length() > 500) {
|
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, field + " must be 500 characters or fewer");
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateIntegerRange(Integer value, int min, int max, String field) {
|
|
||||||
if (value < min || value > max) {
|
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, field + " must be between " + min + " and " + max);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String blankToNull(String value) {
|
|
||||||
return value == null || value.isBlank() ? null : value.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String toDisplayName(String value) {
|
|
||||||
String trimmed = requiredText(value, "name");
|
|
||||||
if (trimmed.length() > 80) {
|
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "name must be 80 characters or fewer");
|
|
||||||
}
|
|
||||||
return trimmed.substring(0, 1).toUpperCase(Locale.ROOT) + trimmed.substring(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
package org.zaine.app.service;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.CurrentCycleDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.PeriodCycleDTO;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.PredictionDTO;
|
|
||||||
import org.zaine.app.model.PeriodCycle;
|
|
||||||
import org.zaine.app.model.PeriodPredictionSnapshot;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class PeriodPredictionService {
|
|
||||||
public static final int DEFAULT_CYCLE_LENGTH = 28;
|
|
||||||
public static final int DEFAULT_PERIOD_LENGTH = 5;
|
|
||||||
|
|
||||||
public PredictionEstimate estimate(List<PeriodCycle> cycles, LocalDate today) {
|
|
||||||
return estimate(cycles, today, DEFAULT_CYCLE_LENGTH, DEFAULT_PERIOD_LENGTH);
|
|
||||||
}
|
|
||||||
|
|
||||||
public PredictionEstimate estimate(List<PeriodCycle> cycles, LocalDate today, int fallbackCycleLength, int fallbackPeriodLength) {
|
|
||||||
int averageCycleLength = averageCycleLength(cycles, fallbackCycleLength);
|
|
||||||
int averagePeriodLength = averagePeriodLength(cycles, fallbackPeriodLength);
|
|
||||||
LocalDate anchor = cycles.stream()
|
|
||||||
.map(PeriodCycle::getStartDate)
|
|
||||||
.max(Comparator.naturalOrder())
|
|
||||||
.orElse(today);
|
|
||||||
|
|
||||||
LocalDate predictedPeriodDate = anchor.plusDays(averageCycleLength);
|
|
||||||
while (!predictedPeriodDate.isAfter(today)) {
|
|
||||||
predictedPeriodDate = predictedPeriodDate.plusDays(averageCycleLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalDate predictedOvulationDate = predictedPeriodDate.minusDays(14);
|
|
||||||
return new PredictionEstimate(
|
|
||||||
predictedPeriodDate,
|
|
||||||
predictedOvulationDate,
|
|
||||||
predictedOvulationDate.minusDays(5),
|
|
||||||
predictedOvulationDate.plusDays(1),
|
|
||||||
averageCycleLength,
|
|
||||||
averagePeriodLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<PredictionEstimate> project(
|
|
||||||
List<PeriodCycle> cycles,
|
|
||||||
LocalDate today,
|
|
||||||
LocalDate from,
|
|
||||||
LocalDate to,
|
|
||||||
int fallbackCycleLength,
|
|
||||||
int fallbackPeriodLength) {
|
|
||||||
PredictionEstimate first = estimate(cycles, today, fallbackCycleLength, fallbackPeriodLength);
|
|
||||||
List<PredictionEstimate> projections = new ArrayList<>();
|
|
||||||
LocalDate predictedPeriodDate = first.predictedPeriodDate();
|
|
||||||
|
|
||||||
while (!predictedPeriodDate.minusDays(19).isAfter(to)) {
|
|
||||||
LocalDate predictedOvulationDate = predictedPeriodDate.minusDays(14);
|
|
||||||
PredictionEstimate projection = new PredictionEstimate(
|
|
||||||
predictedPeriodDate,
|
|
||||||
predictedOvulationDate,
|
|
||||||
predictedOvulationDate.minusDays(5),
|
|
||||||
predictedOvulationDate.plusDays(1),
|
|
||||||
first.averageCycleLength(),
|
|
||||||
first.averagePeriodLength());
|
|
||||||
|
|
||||||
if (intersectsRange(projection, from, to)) {
|
|
||||||
projections.add(projection);
|
|
||||||
}
|
|
||||||
predictedPeriodDate = predictedPeriodDate.plusDays(first.averageCycleLength());
|
|
||||||
}
|
|
||||||
|
|
||||||
return projections;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CurrentCycleDTO currentCycle(List<PeriodCycle> cycles, LocalDate today) {
|
|
||||||
return currentCycle(cycles, today, DEFAULT_CYCLE_LENGTH, DEFAULT_PERIOD_LENGTH);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CurrentCycleDTO currentCycle(List<PeriodCycle> cycles, LocalDate today, int fallbackCycleLength, int fallbackPeriodLength) {
|
|
||||||
PredictionEstimate estimate = estimate(cycles, today, fallbackCycleLength, fallbackPeriodLength);
|
|
||||||
Optional<PeriodCycle> current = cycles.stream()
|
|
||||||
.filter(cycle -> !cycle.getStartDate().isAfter(today))
|
|
||||||
.max(Comparator.comparing(PeriodCycle::getStartDate));
|
|
||||||
|
|
||||||
if (current.isEmpty()) {
|
|
||||||
return new CurrentCycleDTO(null, "Unknown", null, estimate.averageCycleLength(), estimate.averagePeriodLength());
|
|
||||||
}
|
|
||||||
|
|
||||||
PeriodCycle cycle = current.get();
|
|
||||||
int day = (int) ChronoUnit.DAYS.between(cycle.getStartDate(), today) + 1;
|
|
||||||
int ovulationDay = Math.max(1, estimate.averageCycleLength() - 14);
|
|
||||||
int periodLength = cycle.getPeriodLength() != null ? cycle.getPeriodLength() : estimate.averagePeriodLength();
|
|
||||||
|
|
||||||
String phase;
|
|
||||||
if (day <= periodLength) {
|
|
||||||
phase = "Menstrual";
|
|
||||||
} else if (Math.abs(day - ovulationDay) <= 1) {
|
|
||||||
phase = "Ovulation";
|
|
||||||
} else if (day < ovulationDay) {
|
|
||||||
phase = "Follicular";
|
|
||||||
} else {
|
|
||||||
phase = "Luteal";
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CurrentCycleDTO(day, phase, toCycleDto(cycle), estimate.averageCycleLength(), estimate.averagePeriodLength());
|
|
||||||
}
|
|
||||||
|
|
||||||
public PredictionDTO toPredictionDto(PeriodPredictionSnapshot snapshot, PredictionEstimate estimate) {
|
|
||||||
return new PredictionDTO(
|
|
||||||
snapshot.getId(),
|
|
||||||
snapshot.getGeneratedAt(),
|
|
||||||
snapshot.getPredictedPeriodDate(),
|
|
||||||
snapshot.getPredictedOvulationDate(),
|
|
||||||
snapshot.getFertileWindowStart(),
|
|
||||||
snapshot.getFertileWindowEnd(),
|
|
||||||
estimate.averageCycleLength(),
|
|
||||||
estimate.averagePeriodLength());
|
|
||||||
}
|
|
||||||
|
|
||||||
public PeriodCycleDTO toCycleDto(PeriodCycle cycle) {
|
|
||||||
return new PeriodCycleDTO(
|
|
||||||
cycle.getId(),
|
|
||||||
cycle.getStartDate(),
|
|
||||||
cycle.getEndDate(),
|
|
||||||
cycle.getCycleLength(),
|
|
||||||
cycle.getPeriodLength(),
|
|
||||||
cycle.getCreatedAt(),
|
|
||||||
cycle.getUpdatedAt());
|
|
||||||
}
|
|
||||||
|
|
||||||
private int averageCycleLength(List<PeriodCycle> cycles, int fallbackCycleLength) {
|
|
||||||
List<Integer> values = cycles.stream()
|
|
||||||
.map(PeriodCycle::getCycleLength)
|
|
||||||
.filter(length -> length != null && length >= 15 && length <= 60)
|
|
||||||
.toList();
|
|
||||||
return roundedAverage(values, fallbackCycleLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int averagePeriodLength(List<PeriodCycle> cycles, int fallbackPeriodLength) {
|
|
||||||
List<Integer> values = cycles.stream()
|
|
||||||
.map(PeriodCycle::getPeriodLength)
|
|
||||||
.filter(length -> length != null && length >= 1 && length <= 15)
|
|
||||||
.toList();
|
|
||||||
return roundedAverage(values, fallbackPeriodLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int roundedAverage(List<Integer> values, int fallback) {
|
|
||||||
if (values.isEmpty()) {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
double average = values.stream().mapToInt(Integer::intValue).average().orElse(fallback);
|
|
||||||
return (int) Math.round(average);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean intersectsRange(PredictionEstimate projection, LocalDate from, LocalDate to) {
|
|
||||||
LocalDate periodEnd = projection.predictedPeriodDate().plusDays(projection.averagePeriodLength() - 1L);
|
|
||||||
return rangesOverlap(projection.predictedPeriodDate(), periodEnd, from, to)
|
|
||||||
|| rangesOverlap(projection.fertileWindowStart(), projection.fertileWindowEnd(), from, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean rangesOverlap(LocalDate firstStart, LocalDate firstEnd, LocalDate secondStart, LocalDate secondEnd) {
|
|
||||||
return !firstStart.isAfter(secondEnd) && !secondStart.isAfter(firstEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
public record PredictionEstimate(
|
|
||||||
LocalDate predictedPeriodDate,
|
|
||||||
LocalDate predictedOvulationDate,
|
|
||||||
LocalDate fertileWindowStart,
|
|
||||||
LocalDate fertileWindowEnd,
|
|
||||||
int averageCycleLength,
|
|
||||||
int averagePeriodLength) {}
|
|
||||||
}
|
|
||||||
@@ -7,9 +7,8 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.zaine.app.common.application.ApplicationException;
|
||||||
import org.zaine.app.model.TimesheetYear;
|
import org.zaine.app.model.TimesheetYear;
|
||||||
import org.zaine.app.repositories.TimesheetYearRepository;
|
import org.zaine.app.repositories.TimesheetYearRepository;
|
||||||
import org.zaine.app.security.JwtUtil;
|
import org.zaine.app.security.JwtUtil;
|
||||||
@@ -64,20 +63,18 @@ public class TimesheetService {
|
|||||||
if (token != null && jwtUtil.isTokenValid(token)) {
|
if (token != null && jwtUtil.isTokenValid(token)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid or missing credentials");
|
throw ApplicationException.unauthorized("Invalid or missing credentials");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void enforcePayloadSize(JsonNode body) {
|
public void enforcePayloadSize(JsonNode body) {
|
||||||
try {
|
try {
|
||||||
int size = objectMapper.writeValueAsBytes(body).length;
|
int size = objectMapper.writeValueAsBytes(body).length;
|
||||||
if (size > maxPayloadBytes) {
|
if (size > maxPayloadBytes) {
|
||||||
throw new ResponseStatusException(
|
throw ApplicationException.payloadTooLarge(
|
||||||
HttpStatus.PAYLOAD_TOO_LARGE,
|
"Timesheet payload exceeds " + maxPayloadBytes + " bytes");
|
||||||
"Timesheet payload exceeds " + maxPayloadBytes + " bytes"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (JsonProcessingException ex) {
|
} catch (JsonProcessingException ex) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid JSON");
|
throw ApplicationException.badRequest("Invalid JSON");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +118,7 @@ public class TimesheetService {
|
|||||||
ObjectNode normalized = body.isObject() ? (ObjectNode) body.deepCopy() : objectMapper.createObjectNode();
|
ObjectNode normalized = body.isObject() ? (ObjectNode) body.deepCopy() : objectMapper.createObjectNode();
|
||||||
normalized.put("year", year);
|
normalized.put("year", year);
|
||||||
if (!normalized.has("entries")) {
|
if (!normalized.has("entries")) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Payload must include entries object");
|
throw ApplicationException.badRequest("Payload must include entries object");
|
||||||
}
|
}
|
||||||
validateYearPayload(year, normalized);
|
validateYearPayload(year, normalized);
|
||||||
saveRow(year, normalized);
|
saveRow(year, normalized);
|
||||||
@@ -133,7 +130,7 @@ public class TimesheetService {
|
|||||||
enforcePayloadSize(body);
|
enforcePayloadSize(body);
|
||||||
JsonNode incoming = body.has("entries") ? body.get("entries") : body;
|
JsonNode incoming = body.has("entries") ? body.get("entries") : body;
|
||||||
if (!incoming.isObject()) {
|
if (!incoming.isObject()) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Expected entries object");
|
throw ApplicationException.badRequest("Expected entries object");
|
||||||
}
|
}
|
||||||
|
|
||||||
ObjectNode result;
|
ObjectNode result;
|
||||||
@@ -242,16 +239,16 @@ public class TimesheetService {
|
|||||||
row.setSavedAt(java.time.Instant.now());
|
row.setSavedAt(java.time.Instant.now());
|
||||||
repository.save(row);
|
repository.save(row);
|
||||||
} catch (JsonProcessingException ex) {
|
} catch (JsonProcessingException ex) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid JSON");
|
throw ApplicationException.badRequest("Invalid JSON");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateYearPayload(int year, JsonNode body) {
|
private void validateYearPayload(int year, JsonNode body) {
|
||||||
if (!body.has("entries") || !body.get("entries").isObject()) {
|
if (!body.has("entries") || !body.get("entries").isObject()) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Payload must include entries object");
|
throw ApplicationException.badRequest("Payload must include entries object");
|
||||||
}
|
}
|
||||||
if (body.has("year") && body.get("year").asInt() != year) {
|
if (body.has("year") && body.get("year").asInt() != year) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Year mismatch");
|
throw ApplicationException.badRequest("Year mismatch");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +256,7 @@ public class TimesheetService {
|
|||||||
try {
|
try {
|
||||||
return objectMapper.readTree(payload);
|
return objectMapper.readTree(payload);
|
||||||
} catch (JsonProcessingException ex) {
|
} catch (JsonProcessingException ex) {
|
||||||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Corrupt timesheet payload");
|
throw ApplicationException.failure("Corrupt timesheet payload");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package org.zaine.app.service;
|
package org.zaine.app.service;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.zaine.app.dto.WirdEntryDTO;
|
import org.zaine.app.dto.WirdEntryDTO;
|
||||||
import org.zaine.app.model.WirdEntry;
|
import org.zaine.app.model.WirdEntry;
|
||||||
import org.zaine.app.repositories.WirdEntryRepository;
|
import org.zaine.app.repositories.WirdEntryRepository;
|
||||||
import java.lang.System.Logger;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -14,10 +14,12 @@ import java.util.Set;
|
|||||||
@Service
|
@Service
|
||||||
public class WirdService {
|
public class WirdService {
|
||||||
|
|
||||||
private static final Logger logger = System.getLogger(WirdService.class.getName());
|
private static final Logger log = LoggerFactory.getLogger(WirdService.class);
|
||||||
|
private final WirdEntryRepository repo;
|
||||||
|
|
||||||
@Autowired
|
public WirdService(WirdEntryRepository repo) {
|
||||||
private WirdEntryRepository repo;
|
this.repo = repo;
|
||||||
|
}
|
||||||
|
|
||||||
public List<WirdEntry> getAllEntries() {
|
public List<WirdEntry> getAllEntries() {
|
||||||
return repo.findAllByOrderByDateDescCreatedAtDesc();
|
return repo.findAllByOrderByDateDescCreatedAtDesc();
|
||||||
@@ -46,7 +48,7 @@ public class WirdService {
|
|||||||
|
|
||||||
public void deleteEntry(Long id) {
|
public void deleteEntry(Long id) {
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
logger.log(System.Logger.Level.WARNING, "Entry id is null. Operation aborted.");
|
log.warn("Wird entry deletion skipped because id was null");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
repo.deleteById(id);
|
repo.deleteById(id);
|
||||||
|
|||||||
44
src/main/resources/application-dev.properties
Executable file
44
src/main/resources/application-dev.properties
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
spring.datasource.url=${SPRING_DATASOURCE_URL}
|
||||||
|
spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
|
||||||
|
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
|
||||||
|
spring.datasource.driver-class-name=${SPRING_DATASOURCE_DRIVER_CLASS_NAME:org.postgresql.Driver}
|
||||||
|
calibre.db.path=/home/zaine/master-folder/projects/calibre/library/metadata.db
|
||||||
|
|
||||||
|
server.port=9015
|
||||||
|
spring.jpa.show-sql=${SPRING_JPA_SHOW_SQL:false}
|
||||||
|
spring.jpa.hibernate.ddl-auto=none
|
||||||
|
spring.flyway.enabled=true
|
||||||
|
spring.flyway.locations=classpath:db/migration
|
||||||
|
spring.flyway.baseline-on-migrate=true
|
||||||
|
spring.flyway.baseline-version=1
|
||||||
|
timesheet.max-payload-bytes=5242880
|
||||||
|
|
||||||
|
jwt.secret=${JWT_SECRET}
|
||||||
|
jwt.expiration-ms=${JWT_EXPIRATION_MS:86400000}
|
||||||
|
auth.cookie.name=orgWebJwt
|
||||||
|
auth.cookie.secure=false
|
||||||
|
auth.cookie.max-age-seconds=86400
|
||||||
|
org.auth.api-key=${ORG_BACKEND_API_KEY:}
|
||||||
|
timesheet.auth.required=${TIMESHEET_AUTH_REQUIRED:false}
|
||||||
|
|
||||||
|
zone.build.dir=/home/zaine/master-folder/org-platform/org_web
|
||||||
|
zone.build.log=/home/zaine/master-folder/org-platform/org_web/org-web-build.log
|
||||||
|
zone.manifest.path=/home/zaine/master-folder/org-platform/zone/data/manifest.json
|
||||||
|
zone.authoring.url=http://127.0.0.1:8765
|
||||||
|
zone.watcher.unit=watcher.service
|
||||||
|
zone.scripts.log.dir=/home/zaine/logs
|
||||||
|
zone.resources.storage.path=/
|
||||||
|
emacs.run.dir=/home/zaine
|
||||||
|
emacs.run.log=/home/zaine/logs/emacs.log
|
||||||
|
combined.run.log=/home/zaine/logs/combined.log
|
||||||
|
adventure.resources.free.dir=/home/zaine
|
||||||
|
adventure.resources.free.log=/home/zaine/logs/adventure-resources.log
|
||||||
|
adventure.resources.free.command=sudo -n sh -c 'sync; echo 3 > /proc/sys/vm/drop_caches' && /usr/bin/docker system prune -af && /usr/bin/docker builder prune -af
|
||||||
|
guacamole.run.dir=/home/zaine
|
||||||
|
guacamole.container.name=guacamole
|
||||||
|
guacamole.start.log=/home/zaine/logs/guacamole-start.log
|
||||||
|
guacamole.stop.log=/home/zaine/logs/guacamole-stop.log
|
||||||
|
nostalgia.run.log=/home/zaine/logs/nostalgia-prod.log
|
||||||
|
play.rpg.save-dir=/home/zaine/master-folder/org-platform/org_backend/data/rpg-saves
|
||||||
|
|
||||||
|
spring.web.resources.add-mappings=false
|
||||||
44
src/main/resources/application-prod.properties
Executable file
44
src/main/resources/application-prod.properties
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
spring.datasource.url=${SPRING_DATASOURCE_URL}
|
||||||
|
spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
|
||||||
|
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
|
||||||
|
spring.datasource.driver-class-name=${SPRING_DATASOURCE_DRIVER_CLASS_NAME:org.postgresql.Driver}
|
||||||
|
calibre.db.path=/home/zaine/master-folder/projects/calibre/library/metadata.db
|
||||||
|
|
||||||
|
server.port=9010
|
||||||
|
spring.jpa.show-sql=${SPRING_JPA_SHOW_SQL:false}
|
||||||
|
spring.jpa.hibernate.ddl-auto=none
|
||||||
|
spring.flyway.enabled=true
|
||||||
|
spring.flyway.locations=classpath:db/migration
|
||||||
|
# Phase 2 DB had tables before Flyway; baseline records V1 as applied without re-running DDL
|
||||||
|
spring.flyway.baseline-on-migrate=true
|
||||||
|
spring.flyway.baseline-version=1
|
||||||
|
timesheet.max-payload-bytes=5242880
|
||||||
|
|
||||||
|
jwt.secret=${JWT_SECRET}
|
||||||
|
jwt.expiration-ms=${JWT_EXPIRATION_MS:86400000}
|
||||||
|
auth.cookie.name=orgWebJwt
|
||||||
|
auth.cookie.domain=.zainezq.com
|
||||||
|
auth.cookie.secure=true
|
||||||
|
auth.cookie.max-age-seconds=86400
|
||||||
|
org.auth.api-key=${ORG_BACKEND_API_KEY:}
|
||||||
|
timesheet.auth.required=${TIMESHEET_AUTH_REQUIRED:false}
|
||||||
|
|
||||||
|
zone.build.dir=/home/zaine/master-folder/org-platform/org_web
|
||||||
|
zone.build.log=/home/zaine/master-folder/org-platform/org_web/org-web-build.log
|
||||||
|
zone.manifest.path=/home/zaine/master-folder/org-platform/zone/data/manifest.json
|
||||||
|
zone.authoring.url=http://127.0.0.1:8765
|
||||||
|
zone.watcher.unit=watcher.service
|
||||||
|
zone.scripts.log.dir=/home/zaine/logs
|
||||||
|
zone.resources.storage.path=/
|
||||||
|
emacs.run.dir=/home/zaine
|
||||||
|
emacs.run.log=/home/zaine/logs/emacs.log
|
||||||
|
combined.run.log=/home/zaine/logs/combined.log
|
||||||
|
adventure.resources.free.dir=/home/zaine
|
||||||
|
adventure.resources.free.log=/home/zaine/logs/adventure-resources.log
|
||||||
|
adventure.resources.free.command=sudo -n sh -c 'sync; echo 3 > /proc/sys/vm/drop_caches' && /usr/bin/docker system prune -af && /usr/bin/docker builder prune -af
|
||||||
|
guacamole.run.dir=/home/zaine
|
||||||
|
guacamole.container.name=guacamole
|
||||||
|
guacamole.start.log=/home/zaine/logs/guacamole-start.log
|
||||||
|
guacamole.stop.log=/home/zaine/logs/guacamole-stop.log
|
||||||
|
nostalgia.run.log=/home/zaine/logs/nostalgia-prod.log
|
||||||
|
play.rpg.save-dir=/home/zaine/master-folder/org-platform/org_backend/data/rpg-saves
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package org.zaine.app;
|
|
||||||
|
|
||||||
|
|
||||||
public class ApplicationTest {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package org.zaine.app.calibre.application.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.zaine.app.calibre.application.port.out.CalibreCatalogPort;
|
||||||
|
import org.zaine.app.calibre.domain.CalibreBook;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CalibreApplicationServiceTest {
|
||||||
|
@Mock CalibreCatalogPort catalog;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsBooksFromTheExternalCatalogueBoundary() {
|
||||||
|
when(catalog.findAll()).thenReturn(List.of(new CalibreBook(1, "Book", null)));
|
||||||
|
var books = new CalibreApplicationService(catalog).getBooks();
|
||||||
|
assertEquals("", books.get(0).authors());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package org.zaine.app.controller;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
import org.zaine.app.controller.zone.BuildController;
|
||||||
|
import org.zaine.app.service.BuildRunStateService;
|
||||||
|
import org.zaine.app.config.BuildProperties;
|
||||||
|
|
||||||
|
class RetiredEndpointContractTest {
|
||||||
|
private MockMvc mvc;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
mvc = MockMvcBuilders.standaloneSetup(
|
||||||
|
new BuildController(new BuildRunStateService(), mock(BuildProperties.class))).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void periodCompanionAndCheckinRoutesAreNotExposed() throws Exception {
|
||||||
|
mvc.perform(get("/api/period-companion/dashboard")).andExpect(status().isNotFound());
|
||||||
|
mvc.perform(get("/api/checkin/today-status")).andExpect(status().isNotFound());
|
||||||
|
assertThrows(ClassNotFoundException.class, () -> Class.forName("org.zaine.app.controller.PeriodCompanionController"));
|
||||||
|
assertThrows(ClassNotFoundException.class, () -> Class.forName("org.zaine.app.controller.CheckinController"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nostalgiaWriteRoutesAreGoneButStatusReadRemains() throws Exception {
|
||||||
|
mvc.perform(post("/api/nostalgia/production")).andExpect(status().isNotFound());
|
||||||
|
mvc.perform(delete("/api/nostalgia/production")).andExpect(status().isNotFound());
|
||||||
|
mvc.perform(get("/api/nostalgia/production/status"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.running").value(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nostalgiaLogReadMappingRemains() throws Exception {
|
||||||
|
GetMapping mapping = BuildController.class
|
||||||
|
.getMethod("streamNostalgiaProductionLogs")
|
||||||
|
.getAnnotation(GetMapping.class);
|
||||||
|
assertArrayEquals(new String[] {"/nostalgia/production/logs"}, mapping.value());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package org.zaine.app.exception;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
|
import org.zaine.app.common.application.ApplicationException;
|
||||||
|
|
||||||
|
class GlobalExceptionHandlerTest {
|
||||||
|
@Test
|
||||||
|
void mapsApplicationValidationErrorsWithoutLeakingImplementationDetails() {
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/example");
|
||||||
|
|
||||||
|
var response = new GlobalExceptionHandler()
|
||||||
|
.handleApplication(ApplicationException.badRequest("invalid input"), request);
|
||||||
|
|
||||||
|
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||||
|
assertEquals("invalid input", response.getBody().get("message"));
|
||||||
|
assertEquals("/api/example", response.getBody().get("path"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void preservesPayloadTooLargeStatus() {
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/api/timesheet/2026");
|
||||||
|
var response = new GlobalExceptionHandler()
|
||||||
|
.handleApplication(ApplicationException.payloadTooLarge("too large"), request);
|
||||||
|
assertEquals(HttpStatus.PAYLOAD_TOO_LARGE, response.getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package org.zaine.app.notes.application.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.zaine.app.notes.application.port.out.NoteRepositoryPort;
|
||||||
|
import org.zaine.app.notes.domain.Note;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class NotesApplicationServiceTest {
|
||||||
|
@Mock NoteRepositoryPort repository;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsNotesFromThePersistenceBoundary() {
|
||||||
|
when(repository.findAll()).thenReturn(List.of(Note.create("One", "A"), Note.create("Two", "B")));
|
||||||
|
var service = new NotesApplicationService(repository);
|
||||||
|
|
||||||
|
var notes = service.getAll();
|
||||||
|
|
||||||
|
assertEquals(List.of("One", "Two"), notes.stream().map(Note::content).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsAnEmptyListWhenNoNotesExist() {
|
||||||
|
when(repository.findAll()).thenReturn(List.of());
|
||||||
|
assertTrue(new NotesApplicationService(repository).getAll().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createsANoteThroughTheOutputPort() {
|
||||||
|
when(repository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
var service = new NotesApplicationService(repository);
|
||||||
|
|
||||||
|
Note created = service.create("New note", "Author");
|
||||||
|
|
||||||
|
assertEquals("New note", created.content());
|
||||||
|
assertEquals("Author", created.authorName());
|
||||||
|
verify(repository).save(any(Note.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package org.zaine.app.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class BuildRunStateServiceTest {
|
||||||
|
@Test
|
||||||
|
void onlyOneCallerCanStartTheSameCommand() {
|
||||||
|
BuildRunStateService state = new BuildRunStateService();
|
||||||
|
assertTrue(state.tryStart(BuildRunStateService.WEB));
|
||||||
|
assertFalse(state.tryStart(BuildRunStateService.WEB));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void finishingACommandPublishesItsResultAndAllowsAnotherRun() {
|
||||||
|
BuildRunStateService state = new BuildRunStateService();
|
||||||
|
state.tryStart(BuildRunStateService.WEB);
|
||||||
|
state.finish(BuildRunStateService.WEB, 7);
|
||||||
|
|
||||||
|
assertFalse(state.webStatus().running());
|
||||||
|
assertEquals(7, state.webStatus().lastExitCode());
|
||||||
|
assertTrue(state.tryStart(BuildRunStateService.WEB));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nostalgiaRemainsInTheReadOnlyLastRunSnapshot() {
|
||||||
|
assertTrue(new BuildRunStateService().lastRunsSnapshot().containsKey(BuildRunStateService.NOSTALGIA_PROD));
|
||||||
|
}
|
||||||
|
}
|
||||||
139
src/test/java/org/zaine/app/service/CommentsServiceTest.java
Executable file → Normal file
139
src/test/java/org/zaine/app/service/CommentsServiceTest.java
Executable file → Normal file
@@ -1,104 +1,81 @@
|
|||||||
package org.zaine.app.service;
|
package org.zaine.app.service;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import java.util.Arrays;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import java.util.List;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import java.lang.System.Logger;
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.InjectMocks;
|
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.zaine.app.model.Comments;
|
import org.zaine.app.model.Comments;
|
||||||
import org.zaine.app.repositories.CommentsRepository;
|
import org.zaine.app.repositories.CommentsRepository;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.zaine.app.common.application.ApplicationException;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
public class CommentsServiceTest {
|
class CommentsServiceTest {
|
||||||
|
@Mock CommentsRepository repository;
|
||||||
private static final Logger logger = System.getLogger(CompetenciesService.class.getName());
|
|
||||||
|
|
||||||
@InjectMocks
|
|
||||||
private CommentsService commentsService;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private CommentsRepository commentsRepository;
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetAllCommentsReturnsAllComments() {
|
void filtersCommentsByPageSlug() {
|
||||||
|
Comments included = comment("page-1", "Included");
|
||||||
|
Comments excluded = comment("page-2", "Excluded");
|
||||||
|
when(repository.findAll()).thenReturn(List.of(included, excluded));
|
||||||
|
|
||||||
Comments comment1 = new Comments();
|
var result = new CommentsService(repository).getAllCommentsBySlug("page-1");
|
||||||
comment1.setContent("Comment 1");
|
|
||||||
Comments comment2 = new Comments();
|
|
||||||
comment2.setContent("Comment 2");
|
|
||||||
when(commentsRepository.findAll()).thenReturn(Arrays.asList(comment1, comment2));
|
|
||||||
|
|
||||||
List<Comments> result = commentsService.getAllComments();
|
|
||||||
|
|
||||||
assertEquals(2, result.size());
|
|
||||||
assertEquals("Comment 1", result.get(0).getContent());
|
|
||||||
assertEquals("Comment 2", result.get(1).getContent());
|
|
||||||
|
|
||||||
|
assertEquals(List.of(included), result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetAllCommentsBySlugReturnsFilteredComments() {
|
void returnsCommentById() {
|
||||||
String slug = "page-1";
|
Comments comment = comment("page", "Content");
|
||||||
Comments comment1 = new Comments();
|
when(repository.findById(1)).thenReturn(Optional.of(comment));
|
||||||
comment1.setContent("Comment 1");
|
assertEquals(comment, new CommentsService(repository).getCommentById(1));
|
||||||
comment1.setPageSlug("page-1");
|
|
||||||
Comments comment2 = new Comments();
|
|
||||||
comment2.setContent("Comment 2");
|
|
||||||
comment2.setPageSlug("page-2");
|
|
||||||
when(commentsRepository.findAll()).thenReturn(Arrays.asList(comment1, comment2));
|
|
||||||
|
|
||||||
List<Comments> result = commentsService.getAllCommentsBySlug(slug);
|
|
||||||
|
|
||||||
assertEquals(1, result.size());
|
|
||||||
assertEquals("Comment 1", result.get(0).getContent());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetCommentByIdReturnsComment() {
|
void reportsAMissingCommentAsNotFound() {
|
||||||
|
when(repository.findById(9)).thenReturn(Optional.empty());
|
||||||
|
ApplicationException error = assertThrows(ApplicationException.class,
|
||||||
|
() -> new CommentsService(repository).getCommentById(9));
|
||||||
|
assertEquals(ApplicationException.Kind.NOT_FOUND, error.kind());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createsAValidComment() {
|
||||||
|
when(repository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
|
Comments result = new CommentsService(repository).addComment("page", "Author", "Content", null);
|
||||||
|
|
||||||
|
assertEquals("Content", result.getContent());
|
||||||
|
verify(repository).save(any(Comments.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ignoresAnEmptyCommentAsTheExistingApiContractRequires() {
|
||||||
|
Comments result = new CommentsService(repository).addComment("page", "Author", " ", null);
|
||||||
|
assertNull(result);
|
||||||
|
verify(repository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsCommentThread() {
|
||||||
|
Comments reply = comment("page", "Reply");
|
||||||
|
when(repository.findCommentThread(1)).thenReturn(List.of(reply));
|
||||||
|
assertEquals(List.of(reply), new CommentsService(repository).getCommentThread(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Comments comment(String slug, String content) {
|
||||||
Comments comment = new Comments();
|
Comments comment = new Comments();
|
||||||
Integer id = comment.getId();
|
comment.setPageSlug(slug);
|
||||||
if (id == null) {
|
comment.setContent(content);
|
||||||
logger.log(System.Logger.Level.WARNING, "Comment id is null. Operation aborted.");
|
return comment;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
when(commentsRepository.findById(id)).thenReturn(java.util.Optional.of(comment));
|
|
||||||
|
|
||||||
Comments result = commentsService.getCommentById(id);
|
|
||||||
assertSame(comment, result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testAddCommentSavesComment() {
|
|
||||||
Comments comment = new Comments();
|
|
||||||
comment.setContent("New Comment");
|
|
||||||
when(commentsRepository.save(comment)).thenReturn(comment);
|
|
||||||
commentsService.addComment(comment);
|
|
||||||
|
|
||||||
verify(commentsRepository).save(comment);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGetCommentThreadReturnsThread() {
|
|
||||||
Integer commentId = 1;
|
|
||||||
Comments comment1 = new Comments();
|
|
||||||
comment1.setContent("Reply 1");
|
|
||||||
Comments comment2 = new Comments();
|
|
||||||
comment2.setContent("Reply 2");
|
|
||||||
when(commentsRepository.findCommentThread(commentId)).thenReturn(Arrays.asList(comment1, comment2));
|
|
||||||
|
|
||||||
List<Comments> result = commentsService.getCommentThread(commentId);
|
|
||||||
assertEquals(2, result.size());
|
|
||||||
assertEquals("Reply 1", result.get(0).getContent());
|
|
||||||
assertEquals("Reply 2", result.get(1).getContent());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
99
src/test/java/org/zaine/app/service/CompetenciesServiceTest.java
Executable file → Normal file
99
src/test/java/org/zaine/app/service/CompetenciesServiceTest.java
Executable file → Normal file
@@ -1,74 +1,59 @@
|
|||||||
package org.zaine.app.service;
|
package org.zaine.app.service;
|
||||||
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import org.mockito.InjectMocks;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import org.mockito.Mock;
|
import static org.mockito.Mockito.verify;
|
||||||
import org.zaine.app.repositories.CompetenciesRepository;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
import java.util.List;
|
||||||
|
|
||||||
import org.zaine.app.model.Competencies;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
import java.lang.System.Logger;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.zaine.app.model.Competencies;
|
||||||
|
import org.zaine.app.repositories.CompetenciesRepository;
|
||||||
|
import org.zaine.app.common.application.ApplicationException;
|
||||||
|
|
||||||
@ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
public class CompetenciesServiceTest {
|
class CompetenciesServiceTest {
|
||||||
|
@Mock CompetenciesRepository repository;
|
||||||
private static final Logger logger = System.getLogger(CompetenciesService.class.getName());
|
|
||||||
|
|
||||||
@InjectMocks
|
|
||||||
private CompetenciesService competenciesService;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private CompetenciesRepository competenciesRepository;
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetAllCompetencies() {
|
void returnsAllCompetencies() {
|
||||||
Competencies comp1 = new Competencies();
|
Competencies competency = new Competencies();
|
||||||
comp1.setTitle("Competency 1");
|
competency.setTitle("Competency");
|
||||||
Competencies comp2 = new Competencies();
|
when(repository.findAll()).thenReturn(List.of(competency));
|
||||||
comp2.setTitle("Competency 2");
|
assertEquals(List.of(competency), new CompetenciesService(repository).getAllCompetencies());
|
||||||
when(competenciesRepository.findAll()).thenReturn(Arrays.asList(comp1, comp2));
|
|
||||||
|
|
||||||
java.util.List<Competencies> result = competenciesService.getAllCompetencies();
|
|
||||||
assertEquals(2, result.size());
|
|
||||||
assertSame(comp1, result.get(0));
|
|
||||||
assertSame(comp2, result.get(1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetCompetencyByIdReturnsCompetency() {
|
void returnsCompetencyById() {
|
||||||
Competencies comp = new Competencies();
|
Competencies competency = new Competencies();
|
||||||
@NotNull Integer id = comp.getId();
|
when(repository.findById(1)).thenReturn(Optional.of(competency));
|
||||||
if (id == null) {
|
assertEquals(competency, new CompetenciesService(repository).getCompetencyById(1));
|
||||||
logger.log(System.Logger.Level.WARNING, "Competency id is null. Operation aborted.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
when(competenciesRepository.findById(id)).thenReturn(Optional.of(comp));
|
|
||||||
|
|
||||||
|
|
||||||
Competencies result = competenciesService.getCompetencyById(id);
|
|
||||||
assertSame(comp, result);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUpdateCompetencyStatusUpdatesCorrectly() {
|
void reportsAMissingCompetencyAsNotFound() {
|
||||||
Competencies comp = new Competencies();
|
when(repository.findById(9)).thenReturn(Optional.empty());
|
||||||
@NotNull Integer id = comp.getId();
|
ApplicationException error = assertThrows(ApplicationException.class,
|
||||||
if (id == null) {
|
() -> new CompetenciesService(repository).getCompetencyById(9));
|
||||||
logger.log(System.Logger.Level.WARNING, "Competency id is null. Operation aborted.");
|
assertEquals(ApplicationException.Kind.NOT_FOUND, error.kind());
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
when(competenciesRepository.findById(id)).thenReturn(Optional.of(comp));
|
|
||||||
|
|
||||||
competenciesService.updateCompetencyState(id, "completed");
|
@Test
|
||||||
assertEquals("completed", comp.getState());
|
void updatesAnAllowedState() {
|
||||||
verify(competenciesRepository).save(comp);
|
Competencies competency = new Competencies();
|
||||||
|
when(repository.findById(1)).thenReturn(Optional.of(competency));
|
||||||
|
new CompetenciesService(repository).updateCompetencyState(1, "completed");
|
||||||
|
assertEquals("completed", competency.getState());
|
||||||
|
verify(repository).save(competency);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsAnUnknownState() {
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> new CompetenciesService(repository).updateCompetencyState(1, "unknown"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
package org.zaine.app.service;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.mockito.InjectMocks;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.zaine.app.model.Notes;
|
|
||||||
import org.zaine.app.repositories.NotesRepository;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
public class NotesServiceTest {
|
|
||||||
|
|
||||||
@InjectMocks
|
|
||||||
private NotesService notesService;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private NotesRepository notesRepository;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGetAllNotesReturnsAllNotes() {
|
|
||||||
|
|
||||||
Notes note1 = new Notes();
|
|
||||||
note1.setContent("Note 1");
|
|
||||||
Notes note2 = new Notes();
|
|
||||||
note2.setContent("Note 2");
|
|
||||||
when(notesRepository.findAll()).thenReturn(Arrays.asList(note1, note2));
|
|
||||||
|
|
||||||
List<Notes> result = notesService.getAllNotes();
|
|
||||||
|
|
||||||
assertEquals(2, result.size());
|
|
||||||
assertEquals("Note 1", result.get(0).getContent());
|
|
||||||
assertEquals("Note 2", result.get(1).getContent());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGetAllNotesReturnsEmptyListWhenNoNotes() {
|
|
||||||
when(notesRepository.findAll()).thenReturn(Arrays.asList());
|
|
||||||
|
|
||||||
List<Notes> result = notesService.getAllNotes();
|
|
||||||
|
|
||||||
assertTrue(result.isEmpty());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCreateNoteSavesAndReturnsNote() {
|
|
||||||
Notes note = new Notes();
|
|
||||||
note.setContent("New Note");
|
|
||||||
note.setAuthorName("Author");
|
|
||||||
|
|
||||||
when(notesRepository.save(note)).thenReturn(note);
|
|
||||||
|
|
||||||
Notes result = notesService.createNote(note);
|
|
||||||
|
|
||||||
assertEquals("New Note", result.getContent());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package org.zaine.app.service;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.CurrentCycleDTO;
|
|
||||||
import org.zaine.app.model.PeriodCycle;
|
|
||||||
import org.zaine.app.service.PeriodPredictionService.PredictionEstimate;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
|
||||||
|
|
||||||
public class PeriodPredictionServiceTest {
|
|
||||||
private final PeriodPredictionService service = new PeriodPredictionService();
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void estimateUsesDefaultLengthsWhenNoHistoryExists() {
|
|
||||||
LocalDate today = LocalDate.of(2026, 6, 22);
|
|
||||||
|
|
||||||
PredictionEstimate estimate = service.estimate(List.of(), today);
|
|
||||||
|
|
||||||
assertEquals(LocalDate.of(2026, 7, 20), estimate.predictedPeriodDate());
|
|
||||||
assertEquals(LocalDate.of(2026, 7, 6), estimate.predictedOvulationDate());
|
|
||||||
assertEquals(LocalDate.of(2026, 7, 1), estimate.fertileWindowStart());
|
|
||||||
assertEquals(LocalDate.of(2026, 7, 7), estimate.fertileWindowEnd());
|
|
||||||
assertEquals(28, estimate.averageCycleLength());
|
|
||||||
assertEquals(5, estimate.averagePeriodLength());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void estimateUsesConfiguredFallbackLengthsWhenNoHistoryExists() {
|
|
||||||
LocalDate today = LocalDate.of(2026, 6, 22);
|
|
||||||
|
|
||||||
PredictionEstimate estimate = service.estimate(List.of(), today, 32, 6);
|
|
||||||
|
|
||||||
assertEquals(LocalDate.of(2026, 7, 24), estimate.predictedPeriodDate());
|
|
||||||
assertEquals(32, estimate.averageCycleLength());
|
|
||||||
assertEquals(6, estimate.averagePeriodLength());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void estimateUsesHistoricalAveragesForFuturePrediction() {
|
|
||||||
LocalDate today = LocalDate.of(2026, 6, 22);
|
|
||||||
PeriodCycle may = cycle(LocalDate.of(2026, 5, 1), LocalDate.of(2026, 5, 5), 30, 5);
|
|
||||||
PeriodCycle june = cycle(LocalDate.of(2026, 5, 31), LocalDate.of(2026, 6, 4), 28, 5);
|
|
||||||
|
|
||||||
PredictionEstimate estimate = service.estimate(List.of(may, june), today);
|
|
||||||
|
|
||||||
assertEquals(29, estimate.averageCycleLength());
|
|
||||||
assertEquals(LocalDate.of(2026, 6, 29), estimate.predictedPeriodDate());
|
|
||||||
assertEquals(LocalDate.of(2026, 6, 15), estimate.predictedOvulationDate());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void projectReturnsRepeatedPredictionsAcrossLongDateRange() {
|
|
||||||
LocalDate today = LocalDate.of(2026, 6, 22);
|
|
||||||
|
|
||||||
List<PredictionEstimate> projections = service.project(
|
|
||||||
List.of(),
|
|
||||||
today,
|
|
||||||
LocalDate.of(2026, 7, 1),
|
|
||||||
LocalDate.of(2026, 9, 30),
|
|
||||||
28,
|
|
||||||
5);
|
|
||||||
|
|
||||||
assertEquals(4, projections.size());
|
|
||||||
assertEquals(LocalDate.of(2026, 7, 20), projections.get(0).predictedPeriodDate());
|
|
||||||
assertEquals(LocalDate.of(2026, 8, 17), projections.get(1).predictedPeriodDate());
|
|
||||||
assertEquals(LocalDate.of(2026, 9, 14), projections.get(2).predictedPeriodDate());
|
|
||||||
assertEquals(LocalDate.of(2026, 10, 12), projections.get(3).predictedPeriodDate());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void currentCycleReturnsMenstrualPhaseDuringPeriodLength() {
|
|
||||||
PeriodCycle cycle = cycle(LocalDate.of(2026, 6, 20), LocalDate.of(2026, 6, 24), 28, 5);
|
|
||||||
|
|
||||||
CurrentCycleDTO current = service.currentCycle(List.of(cycle), LocalDate.of(2026, 6, 22));
|
|
||||||
|
|
||||||
assertEquals(3, current.day());
|
|
||||||
assertEquals("Menstrual", current.phase());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void currentCycleReturnsUnknownWithoutCycleHistory() {
|
|
||||||
CurrentCycleDTO current = service.currentCycle(List.of(), LocalDate.of(2026, 6, 22));
|
|
||||||
|
|
||||||
assertNull(current.day());
|
|
||||||
assertEquals("Unknown", current.phase());
|
|
||||||
}
|
|
||||||
|
|
||||||
private PeriodCycle cycle(LocalDate startDate, LocalDate endDate, Integer cycleLength, Integer periodLength) {
|
|
||||||
PeriodCycle cycle = new PeriodCycle();
|
|
||||||
cycle.setStartDate(startDate);
|
|
||||||
cycle.setEndDate(endDate);
|
|
||||||
cycle.setCycleLength(cycleLength);
|
|
||||||
cycle.setPeriodLength(periodLength);
|
|
||||||
return cycle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
mock-maker-subclass
|
||||||
Reference in New Issue
Block a user