Implement household domain with outside-in TDD (15 tests)

Controller (5 tests): create household, get mine, get members,
create invite, accept invite.

Service (10 tests): household creation with planner role + seed
data (categories, tags, staple ingredients), conflict when already
in household, invite code generation with 48h expiry, accept invite
with expired/used/conflict validation.

Also includes:
- Household, HouseholdMember, HouseholdInvite JPA entities
- HouseholdInvite repository with findByInviteCode
- Ingredient, IngredientCategory, Tag entities + repositories
  (created early for seed data, will be extended in recipe domain)
- Fixed BackendApplicationTests to use AbstractIntegrationTest

Total: 38 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-01 21:31:00 +02:00
parent 3253dcfec2
commit 4f457303d8
22 changed files with 870 additions and 3 deletions

View File

@@ -0,0 +1,11 @@
package com.recipeapp.recipe;
import com.recipeapp.recipe.entity.IngredientCategory;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.UUID;
public interface IngredientCategoryRepository extends JpaRepository<IngredientCategory, UUID> {
List<IngredientCategory> findByHouseholdIdOrderBySortOrder(UUID householdId);
}

View File

@@ -0,0 +1,11 @@
package com.recipeapp.recipe;
import com.recipeapp.recipe.entity.Ingredient;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.UUID;
public interface IngredientRepository extends JpaRepository<Ingredient, UUID> {
List<Ingredient> findByHouseholdId(UUID householdId);
}

View File

@@ -0,0 +1,11 @@
package com.recipeapp.recipe;
import com.recipeapp.recipe.entity.Tag;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.UUID;
public interface TagRepository extends JpaRepository<Tag, UUID> {
List<Tag> findByHouseholdId(UUID householdId);
}

View File

@@ -0,0 +1,45 @@
package com.recipeapp.recipe.entity;
import com.recipeapp.household.entity.Household;
import jakarta.persistence.*;
import java.util.UUID;
@Entity
@Table(name = "ingredient")
public class Ingredient {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "household_id", nullable = false)
private Household household;
@Column(nullable = false, columnDefinition = "citext")
private String name;
@Column(name = "is_staple", nullable = false)
private boolean isStaple;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private IngredientCategory category;
protected Ingredient() {}
public Ingredient(Household household, String name, boolean isStaple) {
this.household = household;
this.name = name;
this.isStaple = isStaple;
}
public UUID getId() { return id; }
public Household getHousehold() { return household; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public boolean isStaple() { return isStaple; }
public void setStaple(boolean staple) { isStaple = staple; }
public IngredientCategory getCategory() { return category; }
public void setCategory(IngredientCategory category) { this.category = category; }
}

View File

@@ -0,0 +1,47 @@
package com.recipeapp.recipe.entity;
import com.recipeapp.household.entity.Household;
import jakarta.persistence.*;
import java.time.Instant;
import java.util.UUID;
@Entity
@Table(name = "ingredient_category")
public class IngredientCategory {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "household_id", nullable = false)
private Household household;
@Column(nullable = false, columnDefinition = "citext")
private String name;
@Column(name = "sort_order", nullable = false)
private short sortOrder;
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
protected IngredientCategory() {}
public IngredientCategory(Household household, String name, short sortOrder) {
this.household = household;
this.name = name;
this.sortOrder = sortOrder;
}
@PrePersist
void onCreate() { createdAt = 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 getSortOrder() { return sortOrder; }
public void setSortOrder(short sortOrder) { this.sortOrder = sortOrder; }
public Instant getCreatedAt() { return createdAt; }
}

View File

@@ -0,0 +1,46 @@
package com.recipeapp.recipe.entity;
import com.recipeapp.household.entity.Household;
import jakarta.persistence.*;
import java.time.Instant;
import java.util.UUID;
@Entity
@Table(name = "tag")
public class Tag {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "household_id", nullable = false)
private Household household;
@Column(nullable = false, columnDefinition = "citext")
private String name;
@Column(name = "tag_type", nullable = false, length = 20)
private String tagType;
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
protected Tag() {}
public Tag(Household household, String name, String tagType) {
this.household = household;
this.name = name;
this.tagType = tagType;
}
@PrePersist
void onCreate() { createdAt = 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 String getTagType() { return tagType; }
public Instant getCreatedAt() { return createdAt; }
}