Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.bytebite.server;

import com.bytebite.server.dto.GroceryListCreateRequest;
import com.bytebite.server.dto.GroceryListDetailDTO;
import com.bytebite.server.dto.GroceryListSummaryDTO;
import com.bytebite.server.service.GroceryListService;
Expand All @@ -11,11 +12,17 @@
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
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.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.UUID;
Expand Down Expand Up @@ -61,4 +68,44 @@ public GroceryListDetailDTO getById(
@PathVariable UUID id) {
return service.getById(id);
}

@PostMapping(value = "/history",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(
summary = "Create a grocery list",
description = "Persists a new grocery list with its items and returns the created resource.",
security = @SecurityRequirement(name = "bearerAuth"),
responses = {
@ApiResponse(responseCode = "201", description = "Grocery list created"),
@ApiResponse(responseCode = "400", description = "Invalid request body", content = @Content(schema = @Schema(implementation = Map.class))),
@ApiResponse(responseCode = "401", description = "Missing, expired, or invalid JWT", content = @Content)
}
)
public ResponseEntity<GroceryListDetailDTO> create(@RequestBody GroceryListCreateRequest request) {
GroceryListDetailDTO created = service.create(request);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.id())
.toUri();
return ResponseEntity.created(location).body(created);
}

@DeleteMapping(value = "/history/{id}")
@Operation(
summary = "Delete a grocery list",
description = "Deletes the grocery list identified by the given id along with its items.",
security = @SecurityRequirement(name = "bearerAuth"),
responses = {
@ApiResponse(responseCode = "204", description = "Grocery list deleted"),
@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 ResponseEntity<Void> delete(
@Parameter(description = "Identifier of the grocery list", required = true)
@PathVariable UUID id) {
service.delete(id);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.bytebite.server.dto;

public record GroceryItemRequestDTO(String name, double quantity, String unit, String category, boolean purchased) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.bytebite.server.dto;

import java.util.List;
import java.util.UUID;

public record GroceryListCreateRequest(String name, UUID userId, List<GroceryItemRequestDTO> items) {}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.bytebite.server.entity;

import jakarta.persistence.*;
import org.hibernate.annotations.JdbcType;
import org.hibernate.dialect.PostgreSQLEnumJdbcType;
import java.util.UUID;

@Entity
Expand All @@ -21,6 +23,7 @@ public class GroceryItem {
private String unit;

@Enumerated(EnumType.STRING)
@JdbcType(PostgreSQLEnumJdbcType.class)
@Column(columnDefinition = "grocery_category", nullable = false)
private GroceryCategory category;

Expand All @@ -37,4 +40,13 @@ public class GroceryItem {
public String getUnit() { return unit; }
public GroceryCategory getCategory() { return category; }
public boolean isPurchased() { return purchased; }
public GroceryList getGroceryList() { return groceryList; }

public void setId(UUID id) { this.id = id; }
public void setName(String name) { this.name = name; }
public void setQuantity(double quantity) { this.quantity = quantity; }
public void setUnit(String unit) { this.unit = unit; }
public void setCategory(GroceryCategory category) { this.category = category; }
public void setPurchased(boolean purchased) { this.purchased = purchased; }
public void setGroceryList(GroceryList groceryList) { this.groceryList = groceryList; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class GroceryList {
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;

@OneToMany(mappedBy = "groceryList", fetch = FetchType.LAZY)
@OneToMany(mappedBy = "groceryList", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<GroceryItem> items;

public UUID getId() { return id; }
Expand All @@ -34,4 +34,11 @@ public class GroceryList {
public UUID getUserId() { return userId; }
public Instant getCreatedAt() { return createdAt; }
public List<GroceryItem> getItems() { return items; }

public void setId(UUID id) { this.id = id; }
public void setName(String name) { this.name = name; }
public void setOutdated(boolean outdated) { this.outdated = outdated; }
public void setUserId(UUID userId) { this.userId = userId; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public void setItems(List<GroceryItem> items) { this.items = items; }
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
package com.bytebite.server.service;

import com.bytebite.server.dto.GroceryItemRequestDTO;
import com.bytebite.server.dto.GroceryItemResponseDTO;
import com.bytebite.server.dto.GroceryListCreateRequest;
import com.bytebite.server.dto.GroceryListDetailDTO;
import com.bytebite.server.dto.GroceryListSummaryDTO;
import com.bytebite.server.entity.GroceryCategory;
import com.bytebite.server.entity.GroceryItem;
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.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

Expand All @@ -34,7 +40,53 @@ public GroceryListDetailDTO getById(UUID id) {
GroceryList groceryList = repository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"Grocery list not found: " + id));
return toDetail(groceryList);
}

@Transactional
public GroceryListDetailDTO create(GroceryListCreateRequest request) {
if (request.name() == null || request.name().isBlank()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Grocery list name is required");
}
if (request.userId() == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "userId is required");
}

GroceryList groceryList = new GroceryList();
groceryList.setId(UUID.randomUUID());
groceryList.setName(request.name());
groceryList.setUserId(request.userId());
groceryList.setOutdated(false);
groceryList.setCreatedAt(Instant.now());

List<GroceryItem> items = new ArrayList<>();
if (request.items() != null) {
for (GroceryItemRequestDTO itemDto : request.items()) {
GroceryItem item = new GroceryItem();
item.setId(UUID.randomUUID());
item.setName(itemDto.name());
item.setQuantity(itemDto.quantity());
item.setUnit(itemDto.unit());
item.setCategory(parseCategory(itemDto.category()));
item.setPurchased(itemDto.purchased());
item.setGroceryList(groceryList);
items.add(item);
}
}
groceryList.setItems(items);

return toDetail(repository.save(groceryList));
}

@Transactional
public void delete(UUID id) {
if (!repository.existsById(id)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Grocery list not found: " + id);
}
repository.deleteById(id);
}

private GroceryListDetailDTO toDetail(GroceryList groceryList) {
List<GroceryItemResponseDTO> items = groceryList.getItems().stream()
.map(item -> new GroceryItemResponseDTO(
item.getId(),
Expand All @@ -48,4 +100,15 @@ public GroceryListDetailDTO getById(UUID id) {

return new GroceryListDetailDTO(groceryList.getId(), groceryList.getName(), groceryList.getCreatedAt(), items);
}

private GroceryCategory parseCategory(String category) {
if (category == null || category.isBlank()) {
return GroceryCategory.OTHER;
}
try {
return GroceryCategory.valueOf(category.trim().toUpperCase());
} catch (IllegalArgumentException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown grocery category: " + category);
}
}
}
Loading