Implement Recipe, Planning, Shopping, Pantry, and Admin domains

Outside-in TDD for all 5 remaining domains (128 tests total):
- Recipe: CRUD, ingredients autocomplete/patch, tags, categories (27 tests)
- Planning: week plans, slots, confirm, suggestions, variety score, cooking logs (24 tests)
- Shopping: generate from plan, publish, check/add/remove items (15 tests)
- Pantry: CRUD with expiry sorting (11 tests)
- Admin: user management, password reset, audit logging (13 tests)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-01 21:56:51 +02:00
parent 4f457303d8
commit 9ec703abcd
88 changed files with 5267 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
package com.recipeapp.recipe.entity;
import com.recipeapp.household.entity.Household;
import jakarta.persistence.*;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
@Entity
@Table(name = "recipe")
public class Recipe {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "household_id", nullable = false)
private Household household;
@Column(nullable = false, length = 200)
private String name;
@Column(nullable = false)
private short serves;
@Column(name = "cook_time_min", nullable = false)
private short cookTimeMin;
@Column(nullable = false, length = 10)
private String effort;
@Column(name = "is_child_friendly", nullable = false)
private boolean isChildFriendly;
@Column(name = "hero_image_url", length = 500)
private String heroImageUrl;
@Column(name = "deleted_at")
private Instant deletedAt;
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("sortOrder")
private List<RecipeIngredient> ingredients = new ArrayList<>();
@OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("stepNumber")
private List<RecipeStep> steps = new ArrayList<>();
@ManyToMany
@JoinTable(name = "recipe_tag",
joinColumns = @JoinColumn(name = "recipe_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id"))
private Set<Tag> tags = new HashSet<>();
protected Recipe() {}
public Recipe(Household household, String name, short serves, short cookTimeMin,
String effort, boolean isChildFriendly) {
this.household = household;
this.name = name;
this.serves = serves;
this.cookTimeMin = cookTimeMin;
this.effort = effort;
this.isChildFriendly = isChildFriendly;
}
@PrePersist
void onCreate() {
createdAt = Instant.now();
updatedAt = Instant.now();
}
@PreUpdate
void onUpdate() {
updatedAt = Instant.now();
}
public UUID getId() { return id; }
public Household getHousehold() { return household; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public short getServes() { return serves; }
public void setServes(short serves) { this.serves = serves; }
public short getCookTimeMin() { return cookTimeMin; }
public void setCookTimeMin(short cookTimeMin) { this.cookTimeMin = cookTimeMin; }
public String getEffort() { return effort; }
public void setEffort(String effort) { this.effort = effort; }
public boolean isChildFriendly() { return isChildFriendly; }
public void setChildFriendly(boolean childFriendly) { isChildFriendly = childFriendly; }
public String getHeroImageUrl() { return heroImageUrl; }
public void setHeroImageUrl(String heroImageUrl) { this.heroImageUrl = heroImageUrl; }
public Instant getDeletedAt() { return deletedAt; }
public void setDeletedAt(Instant deletedAt) { this.deletedAt = deletedAt; }
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public List<RecipeIngredient> getIngredients() { return ingredients; }
public List<RecipeStep> getSteps() { return steps; }
public Set<Tag> getTags() { return tags; }
public void setTags(Set<Tag> tags) { this.tags = tags; }
public boolean isDeleted() { return deletedAt != null; }
}