diff --git a/databases/grocery-db/init.sql b/databases/grocery-db/init.sql index 9316198..34d3cda 100644 --- a/databases/grocery-db/init.sql +++ b/databases/grocery-db/init.sql @@ -23,7 +23,8 @@ CREATE TABLE IF NOT EXISTS grocery_lists ( grocery_list_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, outdated BOOLEAN NOT NULL DEFAULT FALSE, - user_id UUID NOT NULL + user_id UUID NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS grocery_list_recipes ( diff --git a/databases/user-db/init.sql b/databases/user-db/init.sql index 201fe84..f588637 100644 --- a/databases/user-db/init.sql +++ b/databases/user-db/init.sql @@ -8,3 +8,9 @@ CREATE TABLE IF NOT EXISTS users ( created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); +-- Dev admin account: login with admin@bytebite.dev / password +INSERT INTO users (name, email, password_hash) VALUES ( + 'Admin', + 'admin@bytebite.dev', + '$2b$12$F0rLm1sYDRHEUjQkni8z3e7dpeFNW4irxo4jkMgP4YMGffswtD.OS' +) ON CONFLICT (email) DO NOTHING; diff --git a/server/api-gateway/src/main/resources/application.yml b/server/api-gateway/src/main/resources/application.yml index 543a185..67b10a8 100644 --- a/server/api-gateway/src/main/resources/application.yml +++ b/server/api-gateway/src/main/resources/application.yml @@ -32,7 +32,7 @@ spring: - id: grocery-service uri: ${GROCERY_SERVICE_BASE_URL:http://localhost:8082} predicates: - - Path=/api/recipes/** + - Path=/api/recipes/**,/api/grocery-list/** - id: user-service uri: ${USER_SERVICE_BASE_URL:http://localhost:8083} predicates: diff --git a/server/grocery-service/pom.xml b/server/grocery-service/pom.xml index eb1b6bf..ef8b7f7 100644 --- a/server/grocery-service/pom.xml +++ b/server/grocery-service/pom.xml @@ -42,6 +42,17 @@ ${springdoc-openapi.version} + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.postgresql + postgresql + runtime + + org.springframework.boot spring-boot-starter-test diff --git a/server/grocery-service/src/main/java/com/bytebite/server/GroceryListController.java b/server/grocery-service/src/main/java/com/bytebite/server/GroceryListController.java new file mode 100644 index 0000000..cfc0d80 --- /dev/null +++ b/server/grocery-service/src/main/java/com/bytebite/server/GroceryListController.java @@ -0,0 +1,64 @@ +package com.bytebite.server; + +import com.bytebite.server.dto.GroceryListDetailDTO; +import com.bytebite.server.dto.GroceryListSummaryDTO; +import com.bytebite.server.service.GroceryListService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@RestController +@RequestMapping("/api/grocery-list") +@Tag(name = "Grocery Lists", description = "Stored grocery-list history and details") +public class GroceryListController { + + private final GroceryListService service; + + public GroceryListController(GroceryListService service) { + this.service = service; + } + + @GetMapping(value = "/history", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation( + summary = "List recent grocery lists", + description = "Returns a summary of the 20 most recently created grocery lists, newest first.", + security = @SecurityRequirement(name = "bearerAuth"), + responses = { + @ApiResponse(responseCode = "200", description = "Grocery-list summaries"), + @ApiResponse(responseCode = "401", description = "Missing, expired, or invalid JWT", content = @Content) + } + ) + public List getHistory() { + return service.getHistory(); + } + + @GetMapping(value = "/history/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation( + summary = "Get a grocery list by id", + description = "Returns a single grocery list including its items.", + security = @SecurityRequirement(name = "bearerAuth"), + responses = { + @ApiResponse(responseCode = "200", description = "Grocery list with items"), + @ApiResponse(responseCode = "401", description = "Missing, expired, or invalid JWT", content = @Content), + @ApiResponse(responseCode = "404", description = "Grocery list not found", content = @Content(schema = @Schema(implementation = Map.class))) + } + ) + public GroceryListDetailDTO getById( + @Parameter(description = "Identifier of the grocery list", required = true) + @PathVariable UUID id) { + return service.getById(id); + } +} diff --git a/server/grocery-service/src/main/java/com/bytebite/server/dto/GroceryItemResponseDTO.java b/server/grocery-service/src/main/java/com/bytebite/server/dto/GroceryItemResponseDTO.java new file mode 100644 index 0000000..d5c10f4 --- /dev/null +++ b/server/grocery-service/src/main/java/com/bytebite/server/dto/GroceryItemResponseDTO.java @@ -0,0 +1,5 @@ +package com.bytebite.server.dto; + +import java.util.UUID; + +public record GroceryItemResponseDTO(UUID id, String name, double quantity, String unit, String category, boolean purchased) {} diff --git a/server/grocery-service/src/main/java/com/bytebite/server/dto/GroceryListDetailDTO.java b/server/grocery-service/src/main/java/com/bytebite/server/dto/GroceryListDetailDTO.java new file mode 100644 index 0000000..a3970c3 --- /dev/null +++ b/server/grocery-service/src/main/java/com/bytebite/server/dto/GroceryListDetailDTO.java @@ -0,0 +1,7 @@ +package com.bytebite.server.dto; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +public record GroceryListDetailDTO(UUID id, String name, Instant createdAt, List items) {} diff --git a/server/grocery-service/src/main/java/com/bytebite/server/dto/GroceryListSummaryDTO.java b/server/grocery-service/src/main/java/com/bytebite/server/dto/GroceryListSummaryDTO.java new file mode 100644 index 0000000..06fd30b --- /dev/null +++ b/server/grocery-service/src/main/java/com/bytebite/server/dto/GroceryListSummaryDTO.java @@ -0,0 +1,6 @@ +package com.bytebite.server.dto; + +import java.time.Instant; +import java.util.UUID; + +public record GroceryListSummaryDTO(UUID id, String name, Instant createdAt) {} diff --git a/server/grocery-service/src/main/java/com/bytebite/server/entity/GroceryCategory.java b/server/grocery-service/src/main/java/com/bytebite/server/entity/GroceryCategory.java new file mode 100644 index 0000000..16f800d --- /dev/null +++ b/server/grocery-service/src/main/java/com/bytebite/server/entity/GroceryCategory.java @@ -0,0 +1,14 @@ +package com.bytebite.server.entity; + +public enum GroceryCategory { + PRODUCE, + DAIRY, + MEAT, + SEAFOOD, + BAKERY, + PANTRY, + FROZEN, + BEVERAGES, + SPICES, + OTHER +} diff --git a/server/grocery-service/src/main/java/com/bytebite/server/entity/GroceryItem.java b/server/grocery-service/src/main/java/com/bytebite/server/entity/GroceryItem.java new file mode 100644 index 0000000..2cff949 --- /dev/null +++ b/server/grocery-service/src/main/java/com/bytebite/server/entity/GroceryItem.java @@ -0,0 +1,40 @@ +package com.bytebite.server.entity; + +import jakarta.persistence.*; +import java.util.UUID; + +@Entity +@Table(name = "grocery_items") +public class GroceryItem { + + @Id + @Column(name = "item_id") + private UUID id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private double quantity; + + @Column(nullable = false) + private String unit; + + @Enumerated(EnumType.STRING) + @Column(columnDefinition = "grocery_category", nullable = false) + private GroceryCategory category; + + @Column(name = "is_purchased", nullable = false) + private boolean purchased; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "grocery_list_id") + private GroceryList groceryList; + + public UUID getId() { return id; } + public String getName() { return name; } + public double getQuantity() { return quantity; } + public String getUnit() { return unit; } + public GroceryCategory getCategory() { return category; } + public boolean isPurchased() { return purchased; } +} diff --git a/server/grocery-service/src/main/java/com/bytebite/server/entity/GroceryList.java b/server/grocery-service/src/main/java/com/bytebite/server/entity/GroceryList.java new file mode 100644 index 0000000..8311867 --- /dev/null +++ b/server/grocery-service/src/main/java/com/bytebite/server/entity/GroceryList.java @@ -0,0 +1,37 @@ +package com.bytebite.server.entity; + +import jakarta.persistence.*; +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "grocery_lists") +public class GroceryList { + + @Id + @Column(name = "grocery_list_id") + private UUID id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private boolean outdated; + + @Column(name = "user_id", nullable = false) + private UUID userId; + + @Column(name = "created_at", nullable = false, updatable = false) + private Instant createdAt; + + @OneToMany(mappedBy = "groceryList", fetch = FetchType.LAZY) + private List items; + + public UUID getId() { return id; } + public String getName() { return name; } + public boolean isOutdated() { return outdated; } + public UUID getUserId() { return userId; } + public Instant getCreatedAt() { return createdAt; } + public List getItems() { return items; } +} diff --git a/server/grocery-service/src/main/java/com/bytebite/server/repository/GroceryListRepository.java b/server/grocery-service/src/main/java/com/bytebite/server/repository/GroceryListRepository.java new file mode 100644 index 0000000..3038a99 --- /dev/null +++ b/server/grocery-service/src/main/java/com/bytebite/server/repository/GroceryListRepository.java @@ -0,0 +1,14 @@ +package com.bytebite.server.repository; + +import com.bytebite.server.entity.GroceryList; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.UUID; + +@Repository +public interface GroceryListRepository extends JpaRepository { + + List findTop20ByOrderByCreatedAtDesc(); +} diff --git a/server/grocery-service/src/main/java/com/bytebite/server/service/GroceryListService.java b/server/grocery-service/src/main/java/com/bytebite/server/service/GroceryListService.java new file mode 100644 index 0000000..65ceb17 --- /dev/null +++ b/server/grocery-service/src/main/java/com/bytebite/server/service/GroceryListService.java @@ -0,0 +1,51 @@ +package com.bytebite.server.service; + +import com.bytebite.server.dto.GroceryItemResponseDTO; +import com.bytebite.server.dto.GroceryListDetailDTO; +import com.bytebite.server.dto.GroceryListSummaryDTO; +import com.bytebite.server.entity.GroceryList; +import com.bytebite.server.repository.GroceryListRepository; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; +import java.util.UUID; + +@Service +public class GroceryListService { + + private final GroceryListRepository repository; + + public GroceryListService(GroceryListRepository repository) { + this.repository = repository; + } + + @Transactional(readOnly = true) + public List getHistory() { + return repository.findTop20ByOrderByCreatedAtDesc().stream() + .map(gl -> new GroceryListSummaryDTO(gl.getId(), gl.getName(), gl.getCreatedAt())) + .toList(); + } + + @Transactional(readOnly = true) + public GroceryListDetailDTO getById(UUID id) { + GroceryList groceryList = repository.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, + "Grocery list not found: " + id)); + + List items = groceryList.getItems().stream() + .map(item -> new GroceryItemResponseDTO( + item.getId(), + item.getName(), + item.getQuantity(), + item.getUnit(), + item.getCategory().name(), + item.isPurchased() + )) + .toList(); + + return new GroceryListDetailDTO(groceryList.getId(), groceryList.getName(), groceryList.getCreatedAt(), items); + } +} diff --git a/server/grocery-service/src/main/resources/application.properties b/server/grocery-service/src/main/resources/application.properties index 6920a85..adecbc4 100644 --- a/server/grocery-service/src/main/resources/application.properties +++ b/server/grocery-service/src/main/resources/application.properties @@ -1,3 +1,9 @@ spring.application.name=grocery-service server.port=8082 genai.base-url=http://localhost:8000 + +spring.datasource.url=${GROCERY_DB_URL:jdbc:postgresql://localhost:5432/grocerydb} +spring.datasource.username=${GROCERY_DB_USER:postgres} +spring.datasource.password=${GROCERY_DB_PASSWORD:postgres} +spring.jpa.hibernate.ddl-auto=none +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect