"""Category model. Self-referential (parent_id) for subcategories. `field_schema` is admin-editable JSON describing category-specific listing fields. Shape: { "fields": [ {"name":"condition","label":"Condition","type":"select", "required":true,"options":["new","like_new","good","fair"],"hot":true}, {"name":"brand","label":"Brand","type":"text","required":false,"max":80}, {"name":"salary_min","label":"Min salary","type":"number","required":false,"hot":true} ] } Supported types: text, number, select, bool. `hot:true` marks a field whose value is denormalized onto an indexed Listing column for fast filtering. """ from datetime import datetime from sqlalchemy import JSON from app.extensions import db class Category(db.Model): __tablename__ = "categories" id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"), primary_key=True, autoincrement=True) slug = db.Column(db.String(60), unique=True, nullable=False, index=True) name = db.Column(db.String(80), nullable=False) parent_id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"), db.ForeignKey("categories.id"), nullable=True, index=True) field_schema = db.Column(JSON, nullable=False, default=dict) icon = db.Column(db.String(40), nullable=True) sort_order = db.Column(db.Integer, nullable=False, default=0) sponsor_id = db.Column(db.BigInteger, nullable=True) # FK wired in Phase 5 is_active = db.Column(db.Boolean, nullable=False, default=True) created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) children = db.relationship("Category", backref=db.backref("parent", remote_side=[id]), lazy="selectin") listings = db.relationship("Listing", back_populates="category", lazy="dynamic") @property def is_subcategory(self): return self.parent_id is not None def fields(self): return (self.field_schema or {}).get("fields", []) def hot_fields(self): return [f for f in self.fields() if f.get("hot")] def __repr__(self): return f""