brand-master/PRICE_INHERITANCE.md
dvirlabs 437fe72e48
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Manage contact us messages
2026-05-08 18:40:12 +03:00

368 lines
11 KiB
Markdown

# 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