# Price Inheritance from Models to Products ## Feature Overview Products can now inherit their price from the assigned Model's `base_price` field. This allows you to: 1. Set a base price on a model (e.g., "New Balance 9060" at ₪599) 2. Create product variants (different colors) without specifying price 3. All variants automatically use the model's base price 4. Optionally override the price for specific product variants ## How It Works ### Backend Logic 1. **Product Price Field**: Now nullable - can be `NULL` in database 2. **Price Resolution**: - If product has its own `price` set → use product price - If product `price` is `NULL` AND product is linked to a model → inherit model's `base_price` - If both are `NULL` → defaults to 0.0 (fallback) 3. **Validation**: When creating/updating a product: - If `price` is not provided → `model_id` must be provided - The assigned model must have a `base_price` set - API returns 400 error if validation fails ### API Changes #### Product Create (`POST /api/products`) **Request Body** - Price is now optional: ```json { "name": "New Balance 9060 - Black/Red", "description": "Stylish colorway", "price": null, // ← Can be null now "category_id": 1, "model_id": 5, // ← Required when price is null "brand": "New Balance", "gender": "men", "images": ["..."], "sizes": [] // Can also inherit from model } ``` **Validation**: - ✅ Price provided, model optional → Valid - ✅ Price null, model with base_price provided → Valid (inherits) - ❌ Price null, no model → Error: "Price is required when no model is assigned" - ❌ Price null, model without base_price → Error: "Model must have a base_price set if product price is not provided" **Response**: ```json { "id": 123, "name": "New Balance 9060 - Black/Red", "price": 599.0, // ← Inherited from model's base_price "model_id": 5, ... } ``` #### Product Get (`GET /api/products`, `GET /api/products/{id}`) Products returned with computed price: - If product.price is set → returns product.price - If product.price is NULL → returns model.base_price - Response always includes effective price ### Database Changes **Migration**: `006_make_product_price_nullable.sql` ```sql -- Make price column nullable ALTER TABLE product ALTER COLUMN price DROP NOT NULL; -- Add documentation COMMENT ON COLUMN product.price IS 'Product-specific price. If NULL, inherits from model.base_price'; ``` **To apply migration**: ```bash # Connect to database kubectl exec -it -n my-apps brand-master-db-0 -- psql -U brand_master_user -d brand_master_db # Run migration \i /path/to/migrations/006_make_product_price_nullable.sql # Or use apply-migration script ./apply-migration.sh 006_make_product_price_nullable.sql ``` ### Frontend Changes #### Admin Panel - Product Form **Price Field Behavior**: 1. **No Model Selected**: - Label: "Price *" (required) - Field is required - Placeholder: "Enter price" 2. **Model Selected (with base_price)**: - Label: "Price (Optional - inherits from model)" - Field is optional - Placeholder: "Model price: ₪599.00" - Help text: "Leave empty to use model's base price of ₪599.00" 3. **Model Selected (without base_price)**: - Label: "Price *" (required) - Field is required - Must enter price manually **Form Validation**: - Price field becomes optional dynamically when a model with base_price is selected - JavaScript validation checks: `required={!formData.model_id || !models.find(m => m.id === parseInt(formData.model_id))?.base_price}` ## Usage Examples ### Example 1: Model with Variants (Recommended Use Case) **Step 1**: Create a Model ``` Name: 9060 Brand: New Balance Category: Sneakers Base Price: ₪599 Sizes: 40, 41, 42, 43, 44, 45 ``` **Step 2**: Create Product Variants (without specifying price) ``` Product 1: Name: New Balance 9060 - Black/Red Model: New Balance 9060 Price: (leave empty) → Inherits ₪599 Images: [black-red-1.jpg, black-red-2.jpg] Product 2: Name: New Balance 9060 - White/Blue Model: New Balance 9060 Price: (leave empty) → Inherits ₪599 Images: [white-blue-1.jpg, white-blue-2.jpg] Product 3: Name: New Balance 9060 - Limited Edition Gold Model: New Balance 9060 Price: ₪899 → Override model price Images: [gold-1.jpg, gold-2.jpg] ``` **Result**: - Products 1 & 2: Display at ₪599 (inherited) - Product 3: Displays at ₪899 (overridden) - If you update model's base_price to ₪649, Products 1 & 2 automatically reflect new price - Product 3 remains at ₪899 ### Example 2: Standalone Product (No Model) ``` Product: Name: Nike Air Max Classic Price: ₪399 (required - no model) Model: (none) Category: Sneakers Brand: Nike ``` **Result**: Works as before, price must be specified ### Example 3: Bulk Price Update **Scenario**: You have 10 colorways of "New Balance 9060", all using the model **To update all prices**: 1. Go to Models tab 2. Edit "New Balance 9060" model 3. Change base_price from ₪599 to ₪549 4. Save **Result**: All 10 products now show ₪549 (unless individually overridden) ## Code Changes Summary ### Backend Files Modified 1. **`backend/app/models/product.py`** - Made `price` field nullable - Added `get_effective_price()` helper method 2. **`backend/app/schemas/product.py`** - Changed `ProductCreate.price` from required to `Optional[float]` - Changed `ProductResponse.price` to `Optional[float]` 3. **`backend/app/routers/products.py`** - Added validation in `create_new_product()` endpoint - Added price inheritance logic in all GET endpoints - Added price inheritance in `update_existing_product()` 4. **`backend/migrations/006_make_product_price_nullable.sql`** (NEW) - Database migration to make price column nullable ### Frontend Files Modified 1. **`frontend/src/pages/Admin.jsx`** - Updated `handleSubmit()` to send null price when empty - Enhanced price input field with dynamic placeholder and help text - Made price field conditionally required based on model selection ## Testing Checklist ### Backend Testing - [ ] Create product with price and no model → Works - [ ] Create product with price and model → Works (uses product price) - [ ] Create product without price but with model (has base_price) → Works (inherits) - [ ] Create product without price and no model → Error 400 - [ ] Create product without price with model (no base_price) → Error 400 - [ ] Update product, set model_id, remove price → Inherits model price - [ ] Get product list → Returns computed prices correctly - [ ] Get single product → Returns computed price correctly - [ ] Search products → Returns computed prices correctly ### Frontend Testing - [ ] Create product without model → Price field required - [ ] Select model without base_price → Price field still required - [ ] Select model with base_price → Price field becomes optional - [ ] See model's base_price in placeholder - [ ] Leave price empty → Product created successfully - [ ] Product list shows inherited price correctly - [ ] Edit product with inherited price → Shows empty price field with placeholder - [ ] Update model base_price → All linked products show new price ### Database Testing ```sql -- Check nullable constraint SELECT column_name, is_nullable, data_type FROM information_schema.columns WHERE table_name = 'product' AND column_name = 'price'; -- Expected: is_nullable = 'YES' -- Find products with inherited price SELECT p.id, p.name, p.price as product_price, m.base_price as model_price FROM product p LEFT JOIN model m ON p.model_id = m.id WHERE p.price IS NULL; -- Find models with products SELECT m.id, m.name, m.base_price, COUNT(p.id) as product_count FROM model m LEFT JOIN product p ON p.model_id = m.id GROUP BY m.id, m.name, m.base_price ORDER BY product_count DESC; ``` ## Migration Instructions ### 1. Apply Database Migration ```bash # Option A: Using migration script cd ~/OneDrive/Desktop/gitea/brand-master/backend ./apply-migration.sh migrations/006_make_product_price_nullable.sql # Option B: Manual execution kubectl exec -it -n my-apps brand-master-db-0 -- \ psql -U brand_master_user -d brand_master_db \ -c "ALTER TABLE product ALTER COLUMN price DROP NOT NULL;" ``` ### 2. Deploy Backend Changes ```bash cd ~/OneDrive/Desktop/gitea/brand-master # Build and push backend docker build -t harbor.dvirlabs.com/my-apps/brand-master-backend:latest -f backend/Dockerfile backend/ docker push harbor.dvirlabs.com/my-apps/brand-master-backend:latest ``` ### 3. Deploy Frontend Changes ```bash # Build and push frontend docker build -t harbor.dvirlabs.com/my-apps/brand-master-frontend:latest -f frontend/Dockerfile frontend/ docker push harbor.dvirlabs.com/my-apps/brand-master-frontend:latest ``` ### 4. Upgrade Helm Release ```bash cd ~/OneDrive/Desktop/gitea/my-apps helm upgrade brand-master charts/brand-master-chart \ -f manifests/brand-master/values.yaml \ -n my-apps ``` ### 5. Verify Deployment ```bash # Check pods are running kubectl get pods -n my-apps -l app.kubernetes.io/name=brand-master # Check backend logs kubectl logs -n my-apps -l app.kubernetes.io/component=backend --tail=50 # Test API curl -H "Authorization: Bearer YOUR_TOKEN" \ https://api-brand-master.dvirlabs.com/api/products | jq '.[0] | {id, name, price, model_id}' ``` ## Troubleshooting ### Error: "Price is required when no model is assigned" **Cause**: Trying to create product without price and without model **Solution**: Either: 1. Provide a price value, OR 2. Select a model that has a base_price set ### Error: "Model must have a base_price set if product price is not provided" **Cause**: Selected model doesn't have base_price configured **Solution**: 1. Edit the model and set a base_price, OR 2. Provide a specific price for this product ### Products showing ₪0.00 price **Cause**: Product has no price AND linked model has no base_price **Solution**: 1. Edit the model and set base_price, OR 2. Edit each product and set individual price ### Cannot update existing products **Cause**: Old products might have validation issues **Solution**: Ensure all existing products either have: - A price value set, OR - A model_id linked to a model with base_price ## Benefits ✅ **Easier Product Management**: Create multiple colorways without re-entering price ✅ **Bulk Price Updates**: Change model price → all variants update ✅ **Flexibility**: Can still override price for special editions ✅ **Cleaner Data**: One source of truth for model prices ✅ **Inheritance**: Sizes and price both inherit from models ✅ **Backwards Compatible**: Existing products with prices continue to work ## Future Enhancements Potential improvements for future versions: 1. **Discount Inheritance**: Inherit discount_price from model 2. **Price History**: Track when prices are inherited vs. overridden 3. **Bulk Operations**: Update multiple products to use/remove model price 4. **Price Comparison**: Show difference between model price and overridden price 5. **Reporting**: Analytics on price inheritance usage