All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
368 lines
11 KiB
Markdown
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
|