FunkyJuiceRecipes/SPECS.md
2025-12-18 09:49:31 -06:00

333 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# FunkyJuice Recipes
A python application for managing e-juice recipes
* Uses SQLite3 as database
* Uses Toga as GUI
* Uses Peewee ORM
* Separate files per model
* Separate files per migration
* Migrations for database schema changes using `playhouse.migrate`
* Migrations are versioned via the date they were created (YYYY-MM-DD_migration_name.py)
* Migrations state is stored in a Migrations table in the database
* Migrations table does not use a migration file of its own
* Migrations are run automatically on startup (check migration files vs database entries in migrations table)
* At startup
* The app creates (if missing) the peruser data directory at `app.paths.data`
* The app stores the SQLite DB at `<app.paths.data>/funkyjuicerecipes.data`.
* The connection enables pragmas: `{'foreign_keys': 1, 'journal_mode': 'wal', 'synchronous': 1}`.
* Tests use a temporary DB path.
* Uses class wrappers for Toga widgets
* FunkyJuiceRecipesApp(toga.App)
* MainWindow(toga.MainWindow)
* RecipesTable(toga.Table)
* RecipeForm(toga.Box -> with form elements as needed)
* AddRecipeButton(toga.Button)
* EditRecipeButton(toga.Button)
* DeleteRecipeButton(toga.Button -> with confirmation dialog)
* FlavorsTable(toga.Table)
* AddFlavorButton(toga.Button)
* EditFlavorButton(toga.Button)
* DeleteFlavorButton(toga.Button -> with confirmation dialog)
* FlavorForm(toga.Box -> with form elements as needed)
* Uses briefcase to package app for distribution
* Code follows PEP8
* Code follows SOLID principles
* SRP at method level
## How to Run
* `pip install briefcase`
* or `uv add briefcase`
* `briefcase dev`
* or `python -m funkyjuicerecipes.app`
## Flavors Soft Delete Policy
* When a flavor is deleted
* Flavors are Soft Deleted if they are in a recipe.
* Flavors are Hard Deleted if they are not in a recipe.
* Flavors can be Un-Deleted when viewing the Soft Deleted Flavors list.
* Flavors can be Hard Deleted when viewing the Soft Deleted List and they are no longer in a recipe.
* UI Queries default to `WHERE is_deleted = 0`
* Hard Delete is only allowed when `NOT EXISTS (SELECT 1 FROM recipe_flavor WHERE flavor_id=flavor.id)`
## Directory/File Structure
* project-root/
* pyproject.toml
* README.md
* src/
* funkyjuicerecipes/
* __init__.py
* app.py # Toga App subclass and `main()`
* ui/
* __init__.py
* buttons/
* __init__.py
* add_recipe_button.py
* edit_recipe_button.py
* delete_recipe_button.py
* add_flavor_button.py
* edit_flavor_button.py
* delete_flavor_button.py
* forms/
* __init__.py
* recipe_form.py
* flavor_form.py
* tables/
* __init__.py
* recipes_table.py
* flavors_table.py
* dialogs/
* __init__.py
* delete_recipe_confirmation.py
* delete_flavor_confirmation.py
* data/
* __init__.py
* db.py # database init (Peewee SqliteDatabase, pragmas)
* models/
* __init__.py
* flavor.py
* recipe.py
* recipe_flavor.py
* migrations/
* __init__.py
* 2025-12-18_flavor_table_created.py
* 2025-12-18_recipe_table_created.py
* 2025-12-18_recipe_flavor_table_created.py
* logic/
* __init__.py
* calculations.py # pure functions for PG/VG/nic/flavor math
* resources/ # icons, etc.
* tests/
* test_calculations.py
## Requirements
* Python 3.8+
* Toga
* Peewee
* playhouse.migrate
* Briefcase
## DB Schema
* Enable FKs on connection: `SqliteDatabase(path, pragmas={'foreign_keys': 1})`
* Flavor Table
```
flavor(
id INTEGER PRIMARY KEY,
name TEXT COLLATE NOCASE NOT NULL,
company TEXT COLLATE NOCASE NOT NULL,
base TEXT CHECK(base IN ('PG','VG')) NOT NULL,
is_deleted INTEGER NOT NULL DEFAULT 0,
deleted_at DATETIME NULL,
UNIQUE(name, company)
)
CREATE INDEX IF NOT EXISTS idx_flavor_name_company ON flavor(name, company);
```
* Recipe Table
```
recipe(
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
size_ml INTEGER NOT NULL,
base_pg_pct REAL NOT NULL,
base_vg_pct REAL NOT NULL,
nic_pct REAL CHECK(nic_pct >= 0 AND nic_pct <= 100) NOT NULL,
nic_base TEXT CHECK(nic_base IN ('PG','VG')) NOT NULL,
CHECK (
base_pg_pct >= 0 AND
base_vg_pct >= 0 AND
base_pg_pct + base_vg_pct = 100
)
)
```
* RecipeFlavor XREF Table
```
recipe_flavor(
id INTEGER PRIMARY KEY,
recipe_id INTEGER NOT NULL,
flavor_id INTEGER NOT NULL,
pct REAL CHECK(pct >= 0 AND pct <= 100) NOT NULL,
FOREIGN KEY(recipe_id)
REFERENCES recipe(id) ON DELETE CASCADE,
FOREIGN KEY(flavor_id) REFERENCES flavor(id) ON DELETE RESTRICT,
UNIQUE(recipe_id, flavor_id)
)
CREATE INDEX IF NOT EXISTS idx_recipe_flavor_recipe ON recipe_flavor(recipe_id);
CREATE INDEX IF NOT EXISTS idx_recipe_flavor_flavor ON recipe_flavor(flavor_id);
```
* Migrations Table
```
migrations(
id INTEGER PRIMARY KEY,
batch INTEGER NOT NULL,
filename TEXT NOT NULL,
date_run DATETIME NOT NULL,
UNIQUE(filename)
)
```
## Recipes
* Recipes require
* Name,
* Size (ml),
* PG Base %,
* VG base %,
* Nic Base (PG/VG)
* Nic %,
* Flavor List
* Recipe equations are:
* flavor_pg_pct = sum(pct for flavor in flavors if flavor.base == 'PG')
* flavor_vg_pct = sum(pct for flavor in flavors if flavor.base == 'VG')
* pg_pct = base_pg_pct - flavor_pg_pct - (nic_pct if nic_base == 'PG' else 0)
* vg_pct = base_vg_pct - flavor_vg_pct - (nic_pct if nic_base == 'VG' else 0)
* PG_ml = pg_pct / 100 * size_ml
* VG_ml = vg_pct / 100 * size_ml
* Nic_ml = nic_pct / 100 * size_ml
* Flavor_i_ml = flavor_i.pct / 100 * size_ml
* Recipe output is:
* Recipe Name
* Size (ml)
* PG: PG_ml
* VG: VG_ml
* Nic: Nic_ml
* Flavors
* If viewing individual flavors:
* Flavor 1 Name: Flavor_1_ml
* Flavor 2 Name: Flavor_2_ml
* if viewing Flavor Prep:
* "Flavor Prep": Flavor_Prep_ml (sum(pct for flavor in flavors)/100 * size_ml)
## Main Screen
* Menu Bar with File, Recipes, and Help
* File Menu
* New Recipe
* Exit
* Recipes Menu
* Shows a list of saved recipes as menu items
* Selecting menu item opens the recipe screen
* Help Menu
* About screen
* Shows Saved Recipes List
* Recipe Name
* Button to open the recipe screen (View only)
* Button to edit the recipe (Edit form)
* Button to delete the recipe (Confirmation dialog)
* Has button on top to open Inventory Screen.
## About Screen
* Shows
* Application Name (FunkyJuice Recipes)
* Version Number
* Author (Funky Waddle)
## View Recipe Screen
* Needs access to soft deleted flavors for recipes that currently use deleted flavors.
* Show components rounded to 1 decimal point
* Just convert percentage to decimal number and display, no need to validate sum(amounts) = Size (ml)
* We store the percent in the database, not the actual amount, so there is no check that the amount adds to size (ml), only that sum(percentages) = 100
* Shows recipe name,
* Amount in ml,
* PG: PG_ml, (rounded to 1 decimal point)
* VG: VG_ml, (rounded to 1 decimal point)
* Nic: Nic_ml, (rounded to 1 decimal point)
* Flavors
* If viewing individual flavors:
* Flavor 1 Name: Flavor_1_ml (rounded to 1 decimal point)
* Flavor 2 Name: Flavor_2_ml (rounded to 1 decimal point)
* if viewing Flavor Prep:
* Flavor Prep Amount
* Has button to open Edit Recipe Screen
* Has button to open Delete Recipe
* Has button to open Inventory Screen
* Has Drop Down for "View Recipe Using"
* Options are "Individual Flavors" or "Flavor Prep"
* When Flavor Prep is chosen, it replaces the list of flavors with a total ml of the sum of all the flavors in the recipe.
* When Individual Flavors is chosen (default), it replaces the Flavor Prep with the list of individual flavors.
* Has button for "Mix Flavor Prep"
* Flavor Prep is not stored in database. It's calculated on the fly based on flavors in the recipe.
* Shows recipe of Flavors only, no PG, VG, or Nicotine
* Flavor Prep is a batch of flavors
* Take ratio of each flavor to get total amount of each flavor (12% flavor 1 + 2% flavor 2 = 6:1 ratio)
* Flavor Prep batches of different sizes (10ml, 30ml, 120ml)
* Flavor Prep is just pre-mixing flavors, much like premixing flour and sugar for use in cookie batter.
* You can make 10ml or 1000ml of Sugar Flour, and it won't change the amount used in the cookie recipe.
* You can pre-mix 1000ml of flavors, and use 10ml per recipe, giving you 100 batches of Flavor Prep.
## Edit Recipe Screen
* Needs access to soft deleted flavors for recipes that currently use deleted flavors.
* Has a form for editing the recipe
* Name
* Size (mL) (10ml, 30ml, 120ml)
* PG Base %
* VG Base %
* Nic Base (PG or VG)
* Nic %
* Flavors
* Name (with company)
* Amount %
* If recipe contains a deleted flavor,
* Recipe can be saved with the deleted flavor still in the list, as long as it was not changed.
* Cannot actively change a flavor to a deleted flavor.
* Has button to save changes (Save form, then return to View Recipe screen)
* Has button to cancel changes (Discard form changes, return to View Recipe screen)
* Has button to delete recipe (Confirmation dialog)
* On Save, if recipe contains duplicate flavors,
* If duplicate flavor is found:
* Show question dialog to ask if user wants to "Combine the duplicate flavors" or "Cancel Save" and return to Edit Recipe screen.
* If user chooses to combine duplicate flavors,
* Combine flavors with same name and company into one flavor with combined percentage.
* If user chooses to cancel save,
* DO NOT SAVE and close dialog box, returning user to Edit Recipe screen.
* Validate that (PG Base + VG Base) === 100
* Validate that neither PG Base nor VG Base is < 0
* Validate sum(flavor.pct) + nic.pct + pg.pct + vg.pct ~= 100
* On App Exit, if on Edit Recipe screen and data has changed, ask to save changes before exiting.
* On Screen Change, if on Edit Recipe screen and data has changed, ask to save changes before changing screens.
## Add Recipe Screen
* Has a form for adding a recipe
* Name
* Size (mL) (10ml, 30ml, 120ml)
* PG Base %
* VG Base %
* Nic Base (PG or VG)
* Nic %
* Flavors
* Name
* Amount %
* Has button to save changes (Save form, then return to View Recipe screen)
* Has button to cancel changes (Discard form changes, return to View Recipe screen)
* On Save, if recipe contains duplicate flavors,
* If duplicate flavor is found:
* Show question dialog to ask if user wants to "Combine the duplicate flavors" or "Cancel Save" and return to Edit Recipe screen.
* If user chooses to combine duplicate flavors,
* Combine flavors with same name and company into one flavor with combined percentage.
* If user chooses to cancel save,
* DO NOT SAVE and close dialog box, returning user to Edit Recipe screen.
* Validate that (PG Base + VG Base) === 100
* Validate that neither PG Base nor VG Base is < 0
* Validate sum(flavor.pct) + nic.pct + pg.pct + vg.pct ~= 100
* On App Exit, if on Edit Recipe screen and data has changed, ask to save changes before exiting.
* On Screen Change, if on Edit Recipe screen and data has changed, ask to save changes before changing screens.
## Recipe Form Screens (Add/Edit)
* When adding/editing a recipe, you enter the PG, VG, Flavors, and Nic as %
* PG and VG are entered as % of total recipe size
* When changing PG or VG, the other is updated automatically so that PG + VG is always 100%
* Neither PG or VG can be negative
## Inventory Screen
* List of all Non-Deleted Flavors saved in the database
* Flavor Name
* Company
* PG or VG Base
* Has button to view Soft Deleted Flavors, and how many recipes they are in
* Can Hard Delete Soft Deleted Flavors if they are no longer in a recipe
* Can Un-Delete a Soft Deleted Flavor
* Each Flavor has a button to edit the flavor (Name, Company, PG or VG Base)
* Each Flavor has a button to delete the flavor
* When deleting a flavor,
* If flavor is in a recipe,
* Soft Delete the flavor
* Allow flavor data to be viewed in recipe, but with a strikethrough
* If flavor is not in a recipe,
* Hard Delete the flavor
* NO DUPLICATES ALLOWED