2025-12-18 20:30:09 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import sqlite3
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from types import SimpleNamespace
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from peewee import IntegrityError, DoesNotExist
|
|
|
|
|
|
|
|
|
|
from funkyjuicerecipes.data.db import init_database, DB_FILENAME
|
2025-12-20 06:10:30 +00:00
|
|
|
import funkyjuicerecipes.data.db as dbmod
|
2025-12-18 20:30:09 +00:00
|
|
|
from funkyjuicerecipes.data.dao.flavors import FlavorRepository
|
|
|
|
|
from funkyjuicerecipes.data.dao.recipes import RecipeRepository
|
|
|
|
|
from funkyjuicerecipes.data.models.flavor import Flavor
|
|
|
|
|
from funkyjuicerecipes.data.models.recipe_flavor import RecipeFlavor
|
feat(db,models,dao,logic): nicotine inventory + flavor snapshots; drop legacy nic_pct; migrations, tests
• Migrations
◦ Create nicotine table with NOCASE strings, base PG/VG check, strength_mg_per_ml (>0), soft-delete, and optional inventory metadata (bottle_size_ml, cost, purchase_date).
◦ Alter recipe: add nic_target_mg_per_ml (NOT NULL), nic_strength_mg_per_ml (NOT NULL), nicotine_id (FK ON DELETE SET NULL); keep nic_base. Drop legacy nic_pct via rebuild migration.
◦ Alter flavor: add bottle_size_ml, cost, purchase_date (nullable).
◦ Alter recipe_flavor: add flavor snapshot columns (flavor_name_snapshot, flavor_company_snapshot NOCASE, flavor_base_snapshot PG/VG CHECK).
◦ Ensure migration order runs “_created” before ALTERs.
• Models
◦ Add Nicotine model.
◦ Update Recipe with nic_target_mg_per_ml, nic_strength_mg_per_ml, nic_base, optional nicotine FK.
◦ Update Flavor with optional inventory metadata.
◦ Update RecipeFlavor with snapshot fields and unique index remains.
• DAOs
◦ Add NicotineRepository (CRUD, list with soft-delete filters; hard_delete_when_safe blocked when referenced by recipes).
◦ Update RecipeRepository:
▪ create/update accept nic_target_mg_per_ml, nic_strength_mg_per_ml, nic_base, optional nicotine_id.
▪ If nicotine_id provided, snapshot strength/base from inventory unless overridden.
▪ replace_ingredients uses diff-based updates and populates flavor snapshots on insert.
◦ Update FlavorRepository to edit inventory metadata; soft/undelete; hard delete only when safe.
• DB init
◦ Bind Peewee models to runtime DB; tweak migration ordering logic.
• Logic
◦ Add calculation engine (compute_breakdown, breakdown_for_view) supporting target nicotine mg/mL and stock strength mg/mL; rounding policy for display.
• Tests
◦ Update/init DB tests to include nicotine table and flexible migration counts.
◦ Add calculation tests (120 mL example, nic in VG, 50 mg/mL stock, validations).
◦ Add DAO tests including flavor snapshots stability and replace_ingredients diff behavior.
◦ All tests passing: 15 passed.
• Docs
◦ Update SPECS and MILESTONES; strike through Milestones 3 and 4.
BREAKING CHANGE: remove Recipe.nic_pct field and any compatibility code. API changes in RecipeRepository.create/update now require mg/mL fields and use snapshot+FK model for nicotine.
2025-12-19 01:55:22 +00:00
|
|
|
from funkyjuicerecipes.data.models.flavor import Flavor as FlavorModel
|
2025-12-18 20:30:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class DummyPaths(SimpleNamespace):
|
|
|
|
|
@property
|
|
|
|
|
def data(self) -> Path: # type: ignore[override]
|
|
|
|
|
return self._data
|
|
|
|
|
|
|
|
|
|
@data.setter
|
|
|
|
|
def data(self, value: Path) -> None:
|
|
|
|
|
self._data = Path(value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DummyApp(SimpleNamespace):
|
|
|
|
|
def __init__(self, data_dir: Path) -> None:
|
|
|
|
|
super().__init__()
|
|
|
|
|
self.paths = DummyPaths()
|
|
|
|
|
self.paths.data = data_dir
|
|
|
|
|
|
|
|
|
|
|
2025-12-20 06:10:30 +00:00
|
|
|
def _pkg_db_path() -> Path:
|
|
|
|
|
# New behavior stores DB next to the data module; _db_path_for_app ignores the app argument
|
|
|
|
|
return Path(dbmod._db_path_for_app(None)) # type: ignore[arg-type]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _cleanup_pkg_db() -> None:
|
|
|
|
|
p = _pkg_db_path()
|
|
|
|
|
try:
|
|
|
|
|
if p.exists():
|
|
|
|
|
p.unlink()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2025-12-18 20:30:09 +00:00
|
|
|
def _init_db(tmp_path: Path):
|
2025-12-20 06:10:30 +00:00
|
|
|
# Ensure a clean package-scoped DB for each test
|
|
|
|
|
_cleanup_pkg_db()
|
2025-12-18 20:30:09 +00:00
|
|
|
app = DummyApp(tmp_path / "data")
|
|
|
|
|
db = init_database(app)
|
|
|
|
|
return app, db
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_flavor_uniqueness_nocase(tmp_path: Path):
|
|
|
|
|
app, db = _init_db(tmp_path)
|
|
|
|
|
flavors = FlavorRepository()
|
|
|
|
|
|
|
|
|
|
f1 = flavors.create(name="Strawberry", company="TPA", base="PG")
|
|
|
|
|
assert f1.id is not None
|
|
|
|
|
|
|
|
|
|
with pytest.raises(IntegrityError):
|
|
|
|
|
flavors.create(name="strawberry", company="tpa", base="PG")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_flavor_soft_delete_and_listing_filters(tmp_path: Path):
|
|
|
|
|
app, db = _init_db(tmp_path)
|
|
|
|
|
flavors = FlavorRepository()
|
|
|
|
|
|
|
|
|
|
f1 = flavors.create(name="Vanilla", company="CAP", base="PG")
|
|
|
|
|
f2 = flavors.create(name="Cookie", company="FA", base="PG")
|
|
|
|
|
|
|
|
|
|
# default list excludes deleted
|
|
|
|
|
rows = flavors.list()
|
|
|
|
|
assert [f.name for f in rows] == ["Cookie", "Vanilla"] or [f.name for f in rows] == ["Vanilla", "Cookie"]
|
|
|
|
|
|
|
|
|
|
# soft delete f2
|
|
|
|
|
assert flavors.soft_delete(f2.id) is True
|
|
|
|
|
|
|
|
|
|
rows_default = flavors.list()
|
|
|
|
|
assert all(not r.is_deleted for r in rows_default)
|
|
|
|
|
assert {r.id for r in rows_default} == {f1.id}
|
|
|
|
|
|
|
|
|
|
rows_only_deleted = flavors.list(only_deleted=True)
|
|
|
|
|
assert {r.id for r in rows_only_deleted} == {f2.id}
|
|
|
|
|
|
|
|
|
|
rows_include_deleted = flavors.list(include_deleted=True)
|
|
|
|
|
assert {r.id for r in rows_include_deleted} == {f1.id, f2.id}
|
|
|
|
|
|
|
|
|
|
# undelete brings it back to default list
|
|
|
|
|
assert flavors.undelete(f2.id) is True
|
|
|
|
|
rows_default2 = flavors.list()
|
|
|
|
|
assert {r.id for r in rows_default2} == {f1.id, f2.id}
|
|
|
|
|
|
|
|
|
|
# hard delete safety: referenced flavor cannot be deleted
|
|
|
|
|
recipes = RecipeRepository()
|
|
|
|
|
r = recipes.create(
|
|
|
|
|
name="Test Mix",
|
|
|
|
|
size_ml=30,
|
|
|
|
|
base_pg_pct=50,
|
|
|
|
|
base_vg_pct=50,
|
feat(db,models,dao,logic): nicotine inventory + flavor snapshots; drop legacy nic_pct; migrations, tests
• Migrations
◦ Create nicotine table with NOCASE strings, base PG/VG check, strength_mg_per_ml (>0), soft-delete, and optional inventory metadata (bottle_size_ml, cost, purchase_date).
◦ Alter recipe: add nic_target_mg_per_ml (NOT NULL), nic_strength_mg_per_ml (NOT NULL), nicotine_id (FK ON DELETE SET NULL); keep nic_base. Drop legacy nic_pct via rebuild migration.
◦ Alter flavor: add bottle_size_ml, cost, purchase_date (nullable).
◦ Alter recipe_flavor: add flavor snapshot columns (flavor_name_snapshot, flavor_company_snapshot NOCASE, flavor_base_snapshot PG/VG CHECK).
◦ Ensure migration order runs “_created” before ALTERs.
• Models
◦ Add Nicotine model.
◦ Update Recipe with nic_target_mg_per_ml, nic_strength_mg_per_ml, nic_base, optional nicotine FK.
◦ Update Flavor with optional inventory metadata.
◦ Update RecipeFlavor with snapshot fields and unique index remains.
• DAOs
◦ Add NicotineRepository (CRUD, list with soft-delete filters; hard_delete_when_safe blocked when referenced by recipes).
◦ Update RecipeRepository:
▪ create/update accept nic_target_mg_per_ml, nic_strength_mg_per_ml, nic_base, optional nicotine_id.
▪ If nicotine_id provided, snapshot strength/base from inventory unless overridden.
▪ replace_ingredients uses diff-based updates and populates flavor snapshots on insert.
◦ Update FlavorRepository to edit inventory metadata; soft/undelete; hard delete only when safe.
• DB init
◦ Bind Peewee models to runtime DB; tweak migration ordering logic.
• Logic
◦ Add calculation engine (compute_breakdown, breakdown_for_view) supporting target nicotine mg/mL and stock strength mg/mL; rounding policy for display.
• Tests
◦ Update/init DB tests to include nicotine table and flexible migration counts.
◦ Add calculation tests (120 mL example, nic in VG, 50 mg/mL stock, validations).
◦ Add DAO tests including flavor snapshots stability and replace_ingredients diff behavior.
◦ All tests passing: 15 passed.
• Docs
◦ Update SPECS and MILESTONES; strike through Milestones 3 and 4.
BREAKING CHANGE: remove Recipe.nic_pct field and any compatibility code. API changes in RecipeRepository.create/update now require mg/mL fields and use snapshot+FK model for nicotine.
2025-12-19 01:55:22 +00:00
|
|
|
nic_target_mg_per_ml=0,
|
2025-12-18 20:30:09 +00:00
|
|
|
nic_base="PG",
|
|
|
|
|
ingredients=[(f1.id, 5.0)],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# now f1 is referenced; hard delete should be blocked
|
|
|
|
|
assert flavors.hard_delete_when_safe(f1.id) is False
|
|
|
|
|
# f2 is not referenced; can be hard-deleted
|
|
|
|
|
assert flavors.hard_delete_when_safe(f2.id) is True
|
|
|
|
|
with pytest.raises(DoesNotExist):
|
|
|
|
|
Flavor.get_by_id(f2.id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_recipe_creation_and_get_with_ingredients_includes_soft_deleted_flavors(tmp_path: Path):
|
|
|
|
|
app, db = _init_db(tmp_path)
|
|
|
|
|
flavors = FlavorRepository()
|
|
|
|
|
recipes = RecipeRepository()
|
|
|
|
|
|
|
|
|
|
fa = flavors.create(name="Apple", company="FA", base="PG")
|
|
|
|
|
fb = flavors.create(name="Berry", company="CAP", base="VG")
|
|
|
|
|
|
|
|
|
|
# Soft delete one flavor
|
|
|
|
|
flavors.soft_delete(fb.id)
|
|
|
|
|
|
|
|
|
|
r = recipes.create(
|
|
|
|
|
name="Fruit",
|
|
|
|
|
size_ml=60,
|
|
|
|
|
base_pg_pct=70,
|
|
|
|
|
base_vg_pct=30,
|
feat(db,models,dao,logic): nicotine inventory + flavor snapshots; drop legacy nic_pct; migrations, tests
• Migrations
◦ Create nicotine table with NOCASE strings, base PG/VG check, strength_mg_per_ml (>0), soft-delete, and optional inventory metadata (bottle_size_ml, cost, purchase_date).
◦ Alter recipe: add nic_target_mg_per_ml (NOT NULL), nic_strength_mg_per_ml (NOT NULL), nicotine_id (FK ON DELETE SET NULL); keep nic_base. Drop legacy nic_pct via rebuild migration.
◦ Alter flavor: add bottle_size_ml, cost, purchase_date (nullable).
◦ Alter recipe_flavor: add flavor snapshot columns (flavor_name_snapshot, flavor_company_snapshot NOCASE, flavor_base_snapshot PG/VG CHECK).
◦ Ensure migration order runs “_created” before ALTERs.
• Models
◦ Add Nicotine model.
◦ Update Recipe with nic_target_mg_per_ml, nic_strength_mg_per_ml, nic_base, optional nicotine FK.
◦ Update Flavor with optional inventory metadata.
◦ Update RecipeFlavor with snapshot fields and unique index remains.
• DAOs
◦ Add NicotineRepository (CRUD, list with soft-delete filters; hard_delete_when_safe blocked when referenced by recipes).
◦ Update RecipeRepository:
▪ create/update accept nic_target_mg_per_ml, nic_strength_mg_per_ml, nic_base, optional nicotine_id.
▪ If nicotine_id provided, snapshot strength/base from inventory unless overridden.
▪ replace_ingredients uses diff-based updates and populates flavor snapshots on insert.
◦ Update FlavorRepository to edit inventory metadata; soft/undelete; hard delete only when safe.
• DB init
◦ Bind Peewee models to runtime DB; tweak migration ordering logic.
• Logic
◦ Add calculation engine (compute_breakdown, breakdown_for_view) supporting target nicotine mg/mL and stock strength mg/mL; rounding policy for display.
• Tests
◦ Update/init DB tests to include nicotine table and flexible migration counts.
◦ Add calculation tests (120 mL example, nic in VG, 50 mg/mL stock, validations).
◦ Add DAO tests including flavor snapshots stability and replace_ingredients diff behavior.
◦ All tests passing: 15 passed.
• Docs
◦ Update SPECS and MILESTONES; strike through Milestones 3 and 4.
BREAKING CHANGE: remove Recipe.nic_pct field and any compatibility code. API changes in RecipeRepository.create/update now require mg/mL fields and use snapshot+FK model for nicotine.
2025-12-19 01:55:22 +00:00
|
|
|
nic_target_mg_per_ml=3,
|
2025-12-18 20:30:09 +00:00
|
|
|
nic_base="PG",
|
|
|
|
|
ingredients=[(fa.id, 4.0), (fb.id, 2.0)],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
r2, pairs = recipes.get_with_ingredients(r.id)
|
|
|
|
|
assert r2.id == r.id
|
|
|
|
|
# Both flavors should be present, including the soft-deleted one
|
|
|
|
|
got = {fl.id: pct for (fl, pct) in pairs}
|
|
|
|
|
assert got == {fa.id: 4.0, fb.id: 2.0}
|
|
|
|
|
# Verify the deleted flag is visible on the soft-deleted flavor
|
|
|
|
|
deleted_flavor = next(fl for (fl, pct) in pairs if fl.id == fb.id)
|
|
|
|
|
assert deleted_flavor.is_deleted is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_replace_ingredients_diff_behavior(tmp_path: Path):
|
|
|
|
|
app, db = _init_db(tmp_path)
|
|
|
|
|
flavors = FlavorRepository()
|
|
|
|
|
recipes = RecipeRepository()
|
|
|
|
|
|
|
|
|
|
f1 = flavors.create(name="Lemon", company="FA", base="PG")
|
|
|
|
|
f2 = flavors.create(name="Lime", company="FA", base="PG")
|
|
|
|
|
f3 = flavors.create(name="Orange", company="FA", base="PG")
|
|
|
|
|
|
|
|
|
|
r = recipes.create(
|
|
|
|
|
name="Citrus",
|
|
|
|
|
size_ml=30,
|
|
|
|
|
base_pg_pct=50,
|
|
|
|
|
base_vg_pct=50,
|
feat(db,models,dao,logic): nicotine inventory + flavor snapshots; drop legacy nic_pct; migrations, tests
• Migrations
◦ Create nicotine table with NOCASE strings, base PG/VG check, strength_mg_per_ml (>0), soft-delete, and optional inventory metadata (bottle_size_ml, cost, purchase_date).
◦ Alter recipe: add nic_target_mg_per_ml (NOT NULL), nic_strength_mg_per_ml (NOT NULL), nicotine_id (FK ON DELETE SET NULL); keep nic_base. Drop legacy nic_pct via rebuild migration.
◦ Alter flavor: add bottle_size_ml, cost, purchase_date (nullable).
◦ Alter recipe_flavor: add flavor snapshot columns (flavor_name_snapshot, flavor_company_snapshot NOCASE, flavor_base_snapshot PG/VG CHECK).
◦ Ensure migration order runs “_created” before ALTERs.
• Models
◦ Add Nicotine model.
◦ Update Recipe with nic_target_mg_per_ml, nic_strength_mg_per_ml, nic_base, optional nicotine FK.
◦ Update Flavor with optional inventory metadata.
◦ Update RecipeFlavor with snapshot fields and unique index remains.
• DAOs
◦ Add NicotineRepository (CRUD, list with soft-delete filters; hard_delete_when_safe blocked when referenced by recipes).
◦ Update RecipeRepository:
▪ create/update accept nic_target_mg_per_ml, nic_strength_mg_per_ml, nic_base, optional nicotine_id.
▪ If nicotine_id provided, snapshot strength/base from inventory unless overridden.
▪ replace_ingredients uses diff-based updates and populates flavor snapshots on insert.
◦ Update FlavorRepository to edit inventory metadata; soft/undelete; hard delete only when safe.
• DB init
◦ Bind Peewee models to runtime DB; tweak migration ordering logic.
• Logic
◦ Add calculation engine (compute_breakdown, breakdown_for_view) supporting target nicotine mg/mL and stock strength mg/mL; rounding policy for display.
• Tests
◦ Update/init DB tests to include nicotine table and flexible migration counts.
◦ Add calculation tests (120 mL example, nic in VG, 50 mg/mL stock, validations).
◦ Add DAO tests including flavor snapshots stability and replace_ingredients diff behavior.
◦ All tests passing: 15 passed.
• Docs
◦ Update SPECS and MILESTONES; strike through Milestones 3 and 4.
BREAKING CHANGE: remove Recipe.nic_pct field and any compatibility code. API changes in RecipeRepository.create/update now require mg/mL fields and use snapshot+FK model for nicotine.
2025-12-19 01:55:22 +00:00
|
|
|
nic_target_mg_per_ml=0,
|
2025-12-18 20:30:09 +00:00
|
|
|
nic_base="PG",
|
|
|
|
|
ingredients=[(f1.id, 3.0), (f2.id, 2.0)],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Capture initial rows
|
|
|
|
|
rf_rows = list(
|
|
|
|
|
RecipeFlavor.select().where(RecipeFlavor.recipe == r.id).order_by(RecipeFlavor.id)
|
|
|
|
|
)
|
|
|
|
|
assert len(rf_rows) == 2
|
|
|
|
|
rf_f1 = next(x for x in rf_rows if x.flavor_id == f1.id)
|
|
|
|
|
rf_f2 = next(x for x in rf_rows if x.flavor_id == f2.id)
|
|
|
|
|
|
|
|
|
|
# Now: remove f1, update f2 pct, add f3
|
|
|
|
|
recipes.replace_ingredients(r.id, [(f2.id, 5.5), (f3.id, 1.0)])
|
|
|
|
|
|
|
|
|
|
rf_rows_after = list(
|
|
|
|
|
RecipeFlavor.select().where(RecipeFlavor.recipe == r.id).order_by(RecipeFlavor.id)
|
|
|
|
|
)
|
|
|
|
|
assert len(rf_rows_after) == 2
|
|
|
|
|
flavors_after = {x.flavor_id for x in rf_rows_after}
|
|
|
|
|
assert flavors_after == {f2.id, f3.id}
|
|
|
|
|
|
|
|
|
|
# f2 row should be updated in-place (same id) with new pct
|
|
|
|
|
rf_f2_after = next(x for x in rf_rows_after if x.flavor_id == f2.id)
|
|
|
|
|
assert rf_f2_after.id == rf_f2.id
|
|
|
|
|
assert pytest.approx(rf_f2_after.pct, rel=1e-9) == 5.5
|
|
|
|
|
|
|
|
|
|
# f1 row should be gone
|
|
|
|
|
assert not RecipeFlavor.select().where(
|
|
|
|
|
(RecipeFlavor.recipe == r.id) & (RecipeFlavor.flavor == f1.id)
|
|
|
|
|
).exists()
|
|
|
|
|
|
|
|
|
|
# f3 should be newly present
|
|
|
|
|
assert RecipeFlavor.select().where(
|
|
|
|
|
(RecipeFlavor.recipe == r.id) & (RecipeFlavor.flavor == f3.id)
|
|
|
|
|
).exists()
|
feat(db,models,dao,logic): nicotine inventory + flavor snapshots; drop legacy nic_pct; migrations, tests
• Migrations
◦ Create nicotine table with NOCASE strings, base PG/VG check, strength_mg_per_ml (>0), soft-delete, and optional inventory metadata (bottle_size_ml, cost, purchase_date).
◦ Alter recipe: add nic_target_mg_per_ml (NOT NULL), nic_strength_mg_per_ml (NOT NULL), nicotine_id (FK ON DELETE SET NULL); keep nic_base. Drop legacy nic_pct via rebuild migration.
◦ Alter flavor: add bottle_size_ml, cost, purchase_date (nullable).
◦ Alter recipe_flavor: add flavor snapshot columns (flavor_name_snapshot, flavor_company_snapshot NOCASE, flavor_base_snapshot PG/VG CHECK).
◦ Ensure migration order runs “_created” before ALTERs.
• Models
◦ Add Nicotine model.
◦ Update Recipe with nic_target_mg_per_ml, nic_strength_mg_per_ml, nic_base, optional nicotine FK.
◦ Update Flavor with optional inventory metadata.
◦ Update RecipeFlavor with snapshot fields and unique index remains.
• DAOs
◦ Add NicotineRepository (CRUD, list with soft-delete filters; hard_delete_when_safe blocked when referenced by recipes).
◦ Update RecipeRepository:
▪ create/update accept nic_target_mg_per_ml, nic_strength_mg_per_ml, nic_base, optional nicotine_id.
▪ If nicotine_id provided, snapshot strength/base from inventory unless overridden.
▪ replace_ingredients uses diff-based updates and populates flavor snapshots on insert.
◦ Update FlavorRepository to edit inventory metadata; soft/undelete; hard delete only when safe.
• DB init
◦ Bind Peewee models to runtime DB; tweak migration ordering logic.
• Logic
◦ Add calculation engine (compute_breakdown, breakdown_for_view) supporting target nicotine mg/mL and stock strength mg/mL; rounding policy for display.
• Tests
◦ Update/init DB tests to include nicotine table and flexible migration counts.
◦ Add calculation tests (120 mL example, nic in VG, 50 mg/mL stock, validations).
◦ Add DAO tests including flavor snapshots stability and replace_ingredients diff behavior.
◦ All tests passing: 15 passed.
• Docs
◦ Update SPECS and MILESTONES; strike through Milestones 3 and 4.
BREAKING CHANGE: remove Recipe.nic_pct field and any compatibility code. API changes in RecipeRepository.create/update now require mg/mL fields and use snapshot+FK model for nicotine.
2025-12-19 01:55:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_flavor_snapshots_populated_and_stable(tmp_path: Path):
|
|
|
|
|
app, db = _init_db(tmp_path)
|
|
|
|
|
flavors = FlavorRepository()
|
|
|
|
|
recipes = RecipeRepository()
|
|
|
|
|
|
|
|
|
|
# Create a flavor and recipe using it
|
|
|
|
|
f = flavors.create(name="Vanilla", company="CAP", base="PG")
|
|
|
|
|
r = recipes.create(
|
|
|
|
|
name="Vanilla Mix",
|
|
|
|
|
size_ml=30,
|
|
|
|
|
base_pg_pct=50,
|
|
|
|
|
base_vg_pct=50,
|
|
|
|
|
nic_target_mg_per_ml=0,
|
|
|
|
|
nic_base="PG",
|
|
|
|
|
ingredients=[(f.id, 5.0)],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
rf = RecipeFlavor.get(RecipeFlavor.recipe == r.id)
|
|
|
|
|
assert rf.flavor_id == f.id
|
|
|
|
|
# Snapshots should be populated at creation time
|
|
|
|
|
assert rf.flavor_name_snapshot == "Vanilla"
|
|
|
|
|
assert rf.flavor_company_snapshot == "CAP"
|
|
|
|
|
assert rf.flavor_base_snapshot == "PG"
|
|
|
|
|
|
|
|
|
|
# Change the live Flavor fields
|
|
|
|
|
flavors.update(f.id, name="Vanilla Bean", company="Capella", base="VG")
|
|
|
|
|
|
|
|
|
|
# Snapshots must remain unchanged
|
|
|
|
|
rf2 = RecipeFlavor.get_by_id(rf.id)
|
|
|
|
|
assert rf2.flavor_name_snapshot == "Vanilla"
|
|
|
|
|
assert rf2.flavor_company_snapshot == "CAP"
|
|
|
|
|
assert rf2.flavor_base_snapshot == "PG"
|
|
|
|
|
|
|
|
|
|
# Pct-only update should not touch snapshots
|
|
|
|
|
recipes.replace_ingredients(r.id, [(f.id, 7.5)])
|
|
|
|
|
rf3 = RecipeFlavor.get(RecipeFlavor.recipe == r.id)
|
|
|
|
|
assert rf3.pct == 7.5
|
|
|
|
|
assert rf3.flavor_name_snapshot == "Vanilla"
|
|
|
|
|
assert rf3.flavor_company_snapshot == "CAP"
|
|
|
|
|
assert rf3.flavor_base_snapshot == "PG"
|
|
|
|
|
|
|
|
|
|
# Replacing with a different flavor should capture new snapshots
|
|
|
|
|
f2 = flavors.create(name="Cookie", company="FA", base="VG")
|
|
|
|
|
recipes.replace_ingredients(r.id, [(f2.id, 2.0)])
|
|
|
|
|
rf_after = RecipeFlavor.get(RecipeFlavor.recipe == r.id)
|
|
|
|
|
assert rf_after.flavor_id == f2.id
|
|
|
|
|
assert rf_after.pct == 2.0
|
|
|
|
|
assert rf_after.flavor_name_snapshot == "Cookie"
|
|
|
|
|
assert rf_after.flavor_company_snapshot == "FA"
|
|
|
|
|
assert rf_after.flavor_base_snapshot == "VG"
|