# Product Suppliers Table - Implementation Plan

## Status: Planned

This document outlines the architecture for a dedicated `product_suppliers` table to support multiple suppliers per product with independent pricing.

---

## Problem Statement

Currently, the `Product` model has:
- `supplier` - Single supplier code string
- `supplier_product_id` - Single supplier reference
- `cost_cents` - Single cost value

This limits stores to one supplier per product. In reality:
- A store may source the same product from multiple suppliers
- Each supplier has different costs, lead times, and availability
- The store may want to track cost history and switch suppliers

## Proposed Solution

### New Table: `product_suppliers`

```python
class ProductSupplier(Base, TimestampMixin):
    """Supplier pricing for a store product.

    Allows multiple suppliers per product with independent costs.
    """
    __tablename__ = "product_suppliers"

    id = Column(Integer, primary_key=True, index=True)
    product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True)

    # === SUPPLIER IDENTIFICATION ===
    supplier_code = Column(String(50), nullable=False, index=True)  # 'codeswholesale', 'direct', 'wholesale_partner'
    supplier_name = Column(String(200))  # Human-readable name
    supplier_product_id = Column(String(100))  # Supplier's product reference/SKU
    supplier_product_url = Column(String(500))  # Link to supplier's product page

    # === PRICING (integer cents) ===
    cost_cents = Column(Integer, nullable=False)  # What store pays this supplier
    currency = Column(String(3), default="EUR")

    # === AVAILABILITY ===
    is_active = Column(Boolean, default=True)  # Supplier still offers this product
    is_primary = Column(Boolean, default=False)  # Primary supplier for auto-ordering
    stock_status = Column(String(20))  # 'in_stock', 'low_stock', 'out_of_stock', 'discontinued'
    lead_time_days = Column(Integer)  # Days to receive after ordering
    min_order_quantity = Column(Integer, default=1)

    # === TRACKING ===
    last_price_update = Column(DateTime)  # When cost was last verified
    last_order_date = Column(DateTime)  # Last time ordered from this supplier
    total_orders = Column(Integer, default=0)  # Historical order count

    # === NOTES ===
    notes = Column(Text)  # Internal notes about this supplier relationship

    # === RELATIONSHIPS ===
    product = relationship("Product", back_populates="suppliers")

    # === CONSTRAINTS ===
    __table_args__ = (
        UniqueConstraint("product_id", "supplier_code", name="uq_product_supplier"),
        Index("idx_supplier_product_active", "product_id", "is_active"),
        Index("idx_supplier_code_active", "supplier_code", "is_active"),
    )
```

### Product Model Changes

```python
class Product(Base, TimestampMixin):
    # ... existing fields ...

    # === DEPRECATED (keep for migration, remove later) ===
    # supplier = Column(String(50))  # Deprecated: use suppliers relationship
    # supplier_product_id = Column(String)  # Deprecated: use suppliers relationship

    # === COST (denormalized for performance) ===
    # This is the effective cost, calculated from primary supplier or average
    cost_cents = Column(Integer)  # Keep as denormalized field
    cost_calculation_method = Column(String(20), default="primary")  # 'primary', 'lowest', 'average'

    # === RELATIONSHIPS ===
    suppliers = relationship(
        "ProductSupplier",
        back_populates="product",
        cascade="all, delete-orphan",
        order_by="ProductSupplier.is_primary.desc()"
    )

    # === COMPUTED PROPERTIES ===
    @property
    def primary_supplier(self) -> "ProductSupplier | None":
        """Get the primary supplier for this product."""
        for s in self.suppliers:
            if s.is_primary and s.is_active:
                return s
        # Fallback to first active supplier
        for s in self.suppliers:
            if s.is_active:
                return s
        return None

    @property
    def lowest_cost_cents(self) -> int | None:
        """Get lowest cost across all active suppliers."""
        costs = [s.cost_cents for s in self.suppliers if s.is_active and s.cost_cents]
        return min(costs) if costs else None

    @property
    def average_cost_cents(self) -> int | None:
        """Get average cost across all active suppliers."""
        costs = [s.cost_cents for s in self.suppliers if s.is_active and s.cost_cents]
        return int(sum(costs) / len(costs)) if costs else None

    def update_effective_cost(self) -> None:
        """Update denormalized cost_cents based on calculation method."""
        if self.cost_calculation_method == "primary":
            supplier = self.primary_supplier
            self.cost_cents = supplier.cost_cents if supplier else None
        elif self.cost_calculation_method == "lowest":
            self.cost_cents = self.lowest_cost_cents
        elif self.cost_calculation_method == "average":
            self.cost_cents = self.average_cost_cents
```

---

## Implementation Steps

### Phase 1: Database Schema

1. **Create migration**
   - Add `product_suppliers` table
   - Add `cost_calculation_method` to `products`
   - Keep existing `supplier`, `supplier_product_id`, `cost_cents` fields

2. **Migrate existing data**
   ```sql
   INSERT INTO product_suppliers (product_id, supplier_code, supplier_product_id, cost_cents, is_primary)
   SELECT id, supplier, supplier_product_id, cost_cents, TRUE
   FROM products
   WHERE supplier IS NOT NULL;
   ```

3. **Update models**
   - Add `ProductSupplier` model
   - Add relationship to `Product`
   - Add computed properties

### Phase 2: Service Layer

1. **Create `ProductSupplierService`**
   - `add_supplier(product_id, supplier_data)`
   - `update_supplier(supplier_id, data)`
   - `remove_supplier(supplier_id)`
   - `set_primary_supplier(product_id, supplier_id)`
   - `sync_supplier_costs(supplier_code)` - Bulk update from supplier API

2. **Update `StoreProductService`**
   - Include suppliers in product detail response
   - Update cost calculation on supplier changes

3. **Update import services**
   - CodesWholesale import creates/updates ProductSupplier records
   - Other supplier imports follow same pattern

### Phase 3: API Endpoints

#### Admin Endpoints
```
GET    /api/v1/admin/products/{id}/suppliers       # List suppliers
POST   /api/v1/admin/products/{id}/suppliers       # Add supplier
PUT    /api/v1/admin/products/{id}/suppliers/{sid} # Update supplier
DELETE /api/v1/admin/products/{id}/suppliers/{sid} # Remove supplier
POST   /api/v1/admin/products/{id}/suppliers/{sid}/set-primary
```

#### Store Endpoints
```
GET    /api/v1/store/products/{id}/suppliers
POST   /api/v1/store/products/{id}/suppliers
PUT    /api/v1/store/products/{id}/suppliers/{sid}
DELETE /api/v1/store/products/{id}/suppliers/{sid}
```

### Phase 4: Frontend

1. **Product Detail Page**
   - Suppliers tab/section showing all suppliers
   - Add/edit supplier modal
   - Set primary supplier button
   - Cost comparison view

2. **Bulk Operations**
   - Import suppliers from CSV
   - Sync costs from supplier API
   - Update all products from a supplier

### Phase 5: Cleanup

1. **Remove deprecated fields** (after migration period)
   - Drop `products.supplier`
   - Drop `products.supplier_product_id`

2. **Update documentation**
   - API documentation
   - Architecture docs

---

## Pydantic Schemas

```python
# models/schema/product_supplier.py

class ProductSupplierBase(BaseModel):
    supplier_code: str
    supplier_name: str | None = None
    supplier_product_id: str | None = None
    supplier_product_url: str | None = None
    cost: float  # Euros (converted to cents internally)
    currency: str = "EUR"
    is_active: bool = True
    is_primary: bool = False
    stock_status: str | None = None
    lead_time_days: int | None = None
    min_order_quantity: int = 1
    notes: str | None = None


class ProductSupplierCreate(ProductSupplierBase):
    pass


class ProductSupplierUpdate(BaseModel):
    supplier_name: str | None = None
    supplier_product_id: str | None = None
    supplier_product_url: str | None = None
    cost: float | None = None
    is_active: bool | None = None
    stock_status: str | None = None
    lead_time_days: int | None = None
    min_order_quantity: int | None = None
    notes: str | None = None


class ProductSupplierResponse(ProductSupplierBase):
    id: int
    product_id: int
    cost_cents: int
    last_price_update: datetime | None = None
    last_order_date: datetime | None = None
    total_orders: int = 0
    created_at: datetime
    updated_at: datetime

    class Config:
        from_attributes = True
```

---

## Known Supplier Codes

| Code | Name | Type | Notes |
|------|------|------|-------|
| `codeswholesale` | CodesWholesale | API | Digital game keys |
| `direct` | Direct/Internal | Manual | Store's own inventory |
| `wholesale_partner` | Wholesale Partner | Manual | B2B partner |
| `dropship` | Dropship Supplier | Manual | Ships directly to customer |

---

## Benefits

1. **Multi-supplier support**: Track costs from multiple sources
2. **Price comparison**: See which supplier offers best price
3. **Cost history**: Track price changes over time
4. **Automatic ordering**: Route orders to primary supplier
5. **Supplier analytics**: See order volume per supplier
6. **Graceful transitions**: Switch suppliers without losing data

---

## Estimated Effort

| Phase | Effort | Priority |
|-------|--------|----------|
| Phase 1: Database | 2-3 hours | High |
| Phase 2: Services | 3-4 hours | High |
| Phase 3: API | 2-3 hours | Medium |
| Phase 4: Frontend | 4-6 hours | Medium |
| Phase 5: Cleanup | 1 hour | Low |

**Total: ~12-17 hours**

---

*Plan created: 2025-12-20*
