# Database Design

## Purpose

This app uses a relational database to support five main concerns:

1. Raw material pricing and unit conversion.
2. Mix definitions and mix costing.
3. Product-level formulas and product costing.
4. Mix calculator session history.
5. Access control for both internal users and client users.

The backend is written with SQLAlchemy models in `backend/app/models`. The schema is created automatically at startup, and lightweight migration/patch logic lives in `backend/app/db/migrations.py`.

## Design Principles

- `tenant_id` is the tenancy boundary for most business tables.
- Reference/master data is stored separately from transactional/session data.
- Product costing is built from raw materials -> formulas -> products -> outputs.
- The mix calculator now prefers product-specific ingredient formulas over the shared mix master.
- The database is designed to run on both SQLite locally and Postgres in production.

## High-Level Domains

### 1. Raw Materials

These tables store ingredients and their price history.

- `raw_materials`
  - One row per ingredient/raw material.
  - Stores name, supplier, unit of measure, `kg_per_unit`, status, and notes.
  - Example: `Hulled Oats`, `White French Millet`, `Pano`.

- `raw_material_price_versions`
  - One-to-many from `raw_materials`.
  - Stores `market_value`, `waste_percentage`, `effective_date`, and status.
  - Lets the system keep historical prices instead of overwriting one current value.

Relationship:

- `raw_materials.id` -> `raw_material_price_versions.raw_material_id`

### 2. Mix Master

These tables store shared mix definitions.

- `mixes`
  - One row per named mix.
  - Stores client name, mix name, version, status, and notes.
  - This is the shared mix/master-recipe layer.

- `mix_ingredients`
  - One-to-many from `mixes`.
  - One row per raw material inside a mix.
  - Stores `quantity_kg` for that mix.

Relationships:

- `mixes.id` -> `mix_ingredients.mix_id`
- `raw_materials.id` -> `mix_ingredients.raw_material_id`

Important note:

- This table is still used by mix master pages and as a fallback.
- It is no longer the primary source for mix calculator formulas when product-specific formulas exist.

### 3. Products

These tables describe saleable products and their formula rows.

- `products`
  - One row per sellable product/SKU.
  - Stores client name, product name, optional `item_id`, packaging/unit info, margins, and linked mix.
  - `mix_id` links the product to the shared mix master entry.

- `product_ingredients`
  - One-to-many from `products`.
  - One row per raw material required for that product’s formula.
  - Stores `quantity_kg`, `sort_order`, and optional notes.
  - This is now the key table for the mix calculator.

Relationships:

- `products.mix_id` -> `mixes.id`
- `products.id` -> `product_ingredients.product_id`
- `raw_materials.id` -> `product_ingredients.raw_material_id`

Why both `mix_ingredients` and `product_ingredients` exist:

- `mix_ingredients` represents a shared recipe.
- `product_ingredients` represents the actual formula used for a specific product.
- Multiple products can point at the same mix name but still require product-specific formula rows.
- This solves the workbook case where product labels like `Budgie Mix 20kg` map to a formula/mix name like `Hunter - Budgie Mix`.

### 4. Costing Assumptions

These tables hold non-ingredient costs used in product costing.

- `process_cost_rules`
  - Holds grading, bagging, and cracking costs by `process_name`.

- `packaging_cost_rules`
  - Holds bag cost by `sale_type`, `unit_of_measure`, and `own_bag`.

- `freight_cost_rules`
  - Holds freight cost by `sale_type` and `unit_of_measure`.

These tables are read during product cost calculation after ingredient cost has been resolved.

### 5. Scenarios and Stored Outputs

- `scenarios`
  - Named pricing/costing scenarios.
  - Stores `overrides` as JSON.

- `costing_results`
  - One-to-many from `scenarios`.
  - Stores calculated output per product for a scenario.
  - Includes prices, warnings, and calculation details as JSON.

Relationships:

- `scenarios.id` -> `costing_results.scenario_id`
- `products.id` -> `costing_results.product_id`

### 6. Mix Calculator Sessions

These tables store saved calculator runs.

- `mix_calculator_sessions`
  - Header row for a calculator run.
  - Stores product, mix, batch size, total bags, total kg, prepared by, and timestamps.

- `mix_calculator_session_lines`
  - One-to-many from `mix_calculator_sessions`.
  - Snapshot of the scaled ingredient rows shown to the user at save time.
  - Stores `required_kg`, `mix_percentage`, unit, and display name.

Relationships:

- `mix_calculator_sessions.id` -> `mix_calculator_session_lines.session_id`
- `products.id` -> `mix_calculator_sessions.product_id`
- `mixes.id` -> `mix_calculator_sessions.mix_id`

Important note:

- Session lines are denormalized snapshots.
- They are intentionally stored separately so historical saved runs do not change if product formulas are updated later.

### 7. Client Access / Tenant Administration

These tables manage customer-facing users and feature/module access.

- `client_accounts`
  - One row per client/tenant account.

- `client_users`
  - One-to-many from `client_accounts`.
  - Customer-side users tied to a client account.

- `client_feature_access`
  - One-to-many from `client_accounts`.
  - Feature flags per client account.

- `client_user_module_permissions`
  - One-to-many from `client_users`.
  - Module-level access levels per client user.

- `client_access_audit_events`
  - One-to-many from `client_accounts`.
  - Audit log for client-access changes.

Relationships:

- `client_accounts.id` -> `client_users.client_account_id`
- `client_accounts.id` -> `client_feature_access.client_account_id`
- `client_accounts.id` -> `client_access_audit_events.client_account_id`
- `client_users.id` -> `client_user_module_permissions.client_user_id`

### 8. Internal Access Control

These tables are for internal staff login and permissions.

- `users`
  - Internal users.
  - Stores per-user `password_hash`, role link, and active flag.

- `roles`
  - Named roles like `Admin`, `Operations`, `Full Access`.

- `permissions`
  - Atomic permission keys like `view_mix_calculator`.

- `role_permissions`
  - Many-to-many join table between roles and permissions.

Relationships:

- `roles.id` -> `users.role_id`
- `roles.id` <-> `permissions.id` through `role_permissions`

## Core Costing Flow

### Raw Material Cost

The system calculates ingredient cost from:

- `market_value`
- `waste_percentage`
- `kg_per_unit`

This produces:

- loss cost
- adjusted cost per unit
- cost per kg

### Mix Cost

There are now two formula sources:

1. Preferred: `product_ingredients`
2. Fallback: `mix_ingredients`

For mix calculator and product costing:

- if a product has rows in `product_ingredients`, use them
- otherwise use the linked shared mix from `mix_ingredients`

### Product Cost

Product cost is built from:

1. ingredient formula cost
2. process costs
3. packaging cost
4. freight cost
5. optional distributor / wholesale margin

## Workbook Import Design

The seed/import logic is in `backend/app/seed.py`.

There are now two workbook roles:

- Legacy costing workbook:
  - `C- Raw Products Costs`
  - `M - All`
  - `Product Cost - Price`
- Product-formula workbook:
  - `input_data/1.xlsx`
  - sheet `mix_quantites_per_client_per_pr`

### Current Import Behaviour

- Raw materials are seeded from the legacy costing workbook.
- Shared mixes are seeded from the legacy costing workbook.
- Products are seeded from the legacy costing workbook.
- Product-specific formulas are seeded from `mix_quantites_per_client_per_pr`.

### Formula Matching Rule

Workbook formula rows are attached to products using:

1. `(client_name, product.name)` if it matches directly.
2. `(client_name, product.mix.name)` if the workbook row uses the mix/formula name instead of the sellable product label.

This is important for cases like:

- workbook formula: `HunterBird / Hunter - Budgie Mix`
- product row: `HunterBird / Budgie Mix 20kg`

Both product SKUs can inherit the same formula through the linked mix name.

## Tenancy

Most business tables include `tenant_id`.

This includes:

- raw materials
- price versions
- mixes
- mix ingredients
- product ingredients
- products
- scenarios
- costing results
- mix calculator sessions and lines
- client-access tables
- assumption tables

Startup migration logic backfills `tenant_id` where possible by deriving it from related parent tables.

## Visibility Rules

The `products.visible` flag is used to hide client/product rows from normal UI paths.

Startup migration logic also auto-hides products for a configured list of client names in `backend/app/db/migrations.py`.

This means:

- rows can exist in the database
- but not be offered in normal mix calculator/product selection flows

## Transaction vs Reference Data

Reference/master data:

- `raw_materials`
- `raw_material_price_versions`
- `mixes`
- `mix_ingredients`
- `products`
- `product_ingredients`
- `process_cost_rules`
- `packaging_cost_rules`
- `freight_cost_rules`
- access-control tables

Transactional/snapshot data:

- `mix_calculator_sessions`
- `mix_calculator_session_lines`
- `scenarios`
- `costing_results`
- `client_access_audit_events`

## Important Constraints

- `mix_ingredients` is unique on `(mix_id, raw_material_id)`.
- `product_ingredients` is unique on `(product_id, raw_material_id)`.
- `client_users` is unique on `(client_account_id, email)`.
- `client_feature_access` is unique on `(client_account_id, feature_key)`.
- `client_user_module_permissions` is unique on `(client_user_id, module_key)`.
- `mix_calculator_sessions` is unique on `(tenant_id, session_number)`.

These constraints prevent duplicate ingredient or access rows within the same parent scope.

## Known Tradeoffs

- `RawMaterial.name` is globally unique, not tenant-scoped. That is simple for now, but stricter than a multi-tenant design usually wants.
- `Product.mix_id` is still required even though product-specific formulas now exist. That is useful for compatibility and navigation, but it means a product currently has both a shared mix link and potentially its own formula rows.
- Some calculation outputs are denormalized into session/result tables for stability and history.
- Migration logic is startup-driven and pragmatic rather than using a full migration framework like Alembic.

## Recommended Mental Model

Use this as the working model of the schema:

- `raw_materials` = ingredients
- `raw_material_price_versions` = ingredient pricing history
- `mixes` = shared recipe labels
- `mix_ingredients` = shared recipe lines
- `products` = saleable SKUs
- `product_ingredients` = actual formula for a SKU
- `mix_calculator_sessions` + `lines` = saved production calculations
- `scenarios` + `costing_results` = stored what-if pricing outputs
- `client_*` tables = client account access
- `users / roles / permissions` = internal staff access

## Files To Read Alongside This Document

- [backend/app/models/raw_material.py](/abs/path/C:/Users/mattc/data-entry-app/data-entry-app/backend/app/models/raw_material.py:1)
- [backend/app/models/mix.py](/abs/path/C:/Users/mattc/data-entry-app/data-entry-app/backend/app/models/mix.py:1)
- [backend/app/models/product.py](/abs/path/C:/Users/mattc/data-entry-app/data-entry-app/backend/app/models/product.py:1)
- [backend/app/models/mix_calculator.py](/abs/path/C:/Users/mattc/data-entry-app/data-entry-app/backend/app/models/mix_calculator.py:1)
- [backend/app/models/scenario.py](/abs/path/C:/Users/mattc/data-entry-app/data-entry-app/backend/app/models/scenario.py:1)
- [backend/app/models/assumption.py](/abs/path/C:/Users/mattc/data-entry-app/data-entry-app/backend/app/models/assumption.py:1)
- [backend/app/models/client_access.py](/abs/path/C:/Users/mattc/data-entry-app/data-entry-app/backend/app/models/client_access.py:1)
- [backend/app/models/access.py](/abs/path/C:/Users/mattc/data-entry-app/data-entry-app/backend/app/models/access.py:1)
- [backend/app/db/migrations.py](/abs/path/C:/Users/mattc/data-entry-app/data-entry-app/backend/app/db/migrations.py:1)
- [backend/app/seed.py](/abs/path/C:/Users/mattc/data-entry-app/data-entry-app/backend/app/seed.py:1)
