06/15 Phase 1 + 2 codes

This commit is contained in:
2026-06-15 11:23:05 -04:00
commit c2064b84b4
62 changed files with 2937 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
"""Seed reference data: the four plans, and an optional admin account.
Usage:
python seed.py # seed plans only
python seed.py --admin EMAIL PASS # also create/upgrade an admin user
"""
import sys
from app import create_app
from app.extensions import db
from app.models.plan import Plan
from app.models.user import User
from app.models.category import Category
from app.models.geo import ZipGeo
from app.models.enums import Role
PLANS = [
{
"slug": "free", "name": "Free", "price_monthly_cents": 0, "sort_order": 0,
"config": {
"active_listings": 3, "listing_life_days": 14, "images_per_listing": 3,
"featured_per_month": 0, "auto_bump": None, "analytics": None,
"storefront": False, "verified_badge": False, "ad_free": False,
"scheduled_posting": False, "bulk_csv": False, "priority_support": False,
},
},
{
"slug": "basic", "name": "Basic", "price_monthly_cents": 999, "sort_order": 1,
"config": {
"active_listings": 15, "listing_life_days": 30, "images_per_listing": 8,
"featured_per_month": 1, "auto_bump": None, "analytics": "basic",
"storefront": False, "verified_badge": False, "ad_free": True,
"scheduled_posting": False, "bulk_csv": False, "priority_support": False,
},
},
{
"slug": "pro", "name": "Pro", "price_monthly_cents": 2499, "sort_order": 2,
"config": {
"active_listings": 50, "listing_life_days": 60, "images_per_listing": 15,
"featured_per_month": 5, "auto_bump": "weekly", "analytics": "full",
"storefront": True, "verified_badge": True, "ad_free": True,
"scheduled_posting": True, "bulk_csv": False, "priority_support": False,
},
},
{
"slug": "business", "name": "Business", "price_monthly_cents": 5999, "sort_order": 3,
"config": {
"active_listings": None, "listing_life_days": 90, "images_per_listing": 25,
"featured_per_month": 20, "auto_bump": "daily", "analytics": "full_export",
"storefront": True, "verified_badge": True, "ad_free": True,
"scheduled_posting": True, "bulk_csv": True, "priority_support": True,
},
},
]
def seed_plans():
for p in PLANS:
existing = Plan.query.filter_by(slug=p["slug"]).first()
if existing:
existing.name = p["name"]
existing.price_monthly_cents = p["price_monthly_cents"]
existing.config = p["config"]
existing.sort_order = p["sort_order"]
else:
db.session.add(Plan(**p))
db.session.commit()
print(f"Seeded {len(PLANS)} plans.")
CATEGORIES = [
{"slug": "for-sale", "name": "For Sale", "icon": "tag", "sort_order": 0,
"field_schema": {"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},
]},
"children": ["Furniture", "Electronics", "Cars & Trucks", "Appliances"]},
{"slug": "wanted", "name": "Wanted", "icon": "search", "sort_order": 1,
"field_schema": {"fields": [
{"name": "budget", "label": "Budget (USD)", "type": "number", "required": False, "min": 0},
]},
"children": ["Items Wanted", "Housing Wanted"]},
{"slug": "jobs", "name": "Jobs", "icon": "briefcase", "sort_order": 2,
"field_schema": {"fields": [
{"name": "job_type", "label": "Job type", "type": "select", "required": True,
"options": ["full_time", "part_time", "contract", "temp"], "hot": True},
{"name": "salary_min", "label": "Salary min (USD/yr)", "type": "number",
"required": False, "min": 0, "hot": True},
{"name": "salary_max", "label": "Salary max (USD/yr)", "type": "number",
"required": False, "min": 0, "hot": True},
{"name": "remote", "label": "Remote", "type": "bool", "required": False},
]},
"children": ["Restaurant", "Salon & Spa", "Construction", "Office"]},
{"slug": "services", "name": "Services", "icon": "tools", "sort_order": 3,
"field_schema": {"fields": [
{"name": "rate", "label": "Rate (USD/hr)", "type": "number", "required": False, "min": 0},
]},
"children": ["Home Services", "Beauty", "Tutoring", "Auto Repair"]},
{"slug": "supplies", "name": "Supplies", "icon": "box", "sort_order": 4,
"field_schema": {"fields": [
{"name": "min_order", "label": "Min order qty", "type": "number", "required": False, "min": 1},
{"name": "condition", "label": "Condition", "type": "select", "required": False,
"options": ["new", "used"], "hot": True},
]},
"children": ["Wholesale", "Restaurant Supply", "Salon Supply"]},
{"slug": "community", "name": "Community", "icon": "users", "sort_order": 5,
"field_schema": {"fields": [
{"name": "event_date", "label": "Event date", "type": "text", "required": False, "max": 40},
]},
"children": ["Events", "Announcements", "Free Stuff", "Classes"]},
]
# Sample ZIPs for Vietnamese/Hispanic hubs (replace with full SimpleMaps/Census dataset).
ZIP_SAMPLE = [
("92683", "Westminster", "CA", 33.7513, -117.9939, "Orange County"),
("92840", "Garden Grove", "CA", 33.7886, -117.9390, "Orange County"),
("95111", "San Jose", "CA", 37.2956, -121.8200, "San Jose"),
("95122", "San Jose", "CA", 37.3300, -121.8350, "San Jose"),
("77036", "Houston", "TX", 29.7045, -95.5380, "Houston"),
("77072", "Houston", "TX", 29.7060, -95.5930, "Houston"),
("78228", "San Antonio", "TX", 29.4630, -98.5790, "San Antonio"),
("33125", "Miami", "FL", 25.7840, -80.2370, "Miami"),
("90250", "Hawthorne", "CA", 33.9130, -118.3490, "Los Angeles"),
("22041", "Falls Church", "VA", 38.8480, -77.1370, "DC Metro"),
]
def seed_categories():
for c in CATEGORIES:
parent = Category.query.filter_by(slug=c["slug"]).first()
if parent is None:
parent = Category(slug=c["slug"], name=c["name"], icon=c.get("icon"),
sort_order=c["sort_order"], field_schema=c["field_schema"])
db.session.add(parent)
db.session.flush()
else:
parent.field_schema = c["field_schema"]
parent.sort_order = c["sort_order"]
for i, child_name in enumerate(c.get("children", [])):
cslug = f"{c['slug']}-{child_name.lower().replace(' & ', '-').replace(' ', '-')}"
if Category.query.filter_by(slug=cslug).first() is None:
db.session.add(Category(slug=cslug, name=child_name,
parent_id=parent.id, sort_order=i,
field_schema=c["field_schema"]))
db.session.commit()
print(f"Seeded {len(CATEGORIES)} top categories + subcategories.")
def seed_zip_sample():
for z, city, st, lat, lng, metro in ZIP_SAMPLE:
if ZipGeo.query.get(z) is None:
db.session.add(ZipGeo(zip=z, city=city, state=st, lat=lat, lng=lng, metro=metro))
db.session.commit()
print(f"Seeded {len(ZIP_SAMPLE)} sample ZIP rows.")
def make_admin(email, password):
user = User.query.filter_by(email=email.lower()).first()
if user is None:
user = User(email=email.lower(), display_name="Admin", role=Role.admin,
email_verified=True)
user.set_password(password)
db.session.add(user)
else:
user.role = Role.admin
user.email_verified = True
user.set_password(password)
db.session.commit()
print(f"Admin ready: {email}")
if __name__ == "__main__":
app = create_app()
with app.app_context():
seed_plans()
seed_categories()
seed_zip_sample()
if "--admin" in sys.argv:
i = sys.argv.index("--admin")
make_admin(sys.argv[i + 1], sys.argv[i + 2])