import os import pathlib # Load .env FIRST so auth/db/vault see their keys at import time. _ENV = pathlib.Path(__file__).resolve().parent.parent / ".env" if _ENV.exists(): for _ln in _ENV.read_text().splitlines(): _ln = _ln.strip() if _ln and not _ln.startswith("#") and "=" in _ln: _k, _v = _ln.split("=", 1) os.environ.setdefault(_k.strip(), _v.strip()) from fastapi import FastAPI # noqa: E402 from fastapi.staticfiles import StaticFiles # noqa: E402 from fastapi.responses import FileResponse # noqa: E402 from . import __version__ # noqa: E402 from .wowplatter_v1 import router as wowplatter_v1_router # noqa: E402 from .virtual import router as virtual_router # noqa: E402 from .settings_routes import router as settings_router # noqa: E402 from .admin_routes import router as admin_router # noqa: E402 from .layout_routes import router as layout_router # noqa: E402 from .shop_routes import router as shop_router # noqa: E402 from .sales_routes import router as sales_router # noqa: E402 from .db import engine # noqa: E402 from sqlalchemy import text as _sqltext # noqa: E402 app = FastAPI(title="RECORDGOD", version=__version__) app.include_router(wowplatter_v1_router) app.include_router(virtual_router) app.include_router(settings_router) app.include_router(admin_router) app.include_router(layout_router) app.include_router(shop_router) app.include_router(sales_router) _STARTUP_DDL = [ "CREATE TABLE IF NOT EXISTS store_config (store_id int PRIMARY KEY, config jsonb NOT NULL, updated_at timestamptz NOT NULL DEFAULT now())", # POS reference tables (populated by migrate.py; created here so prod has them on deploy) "CREATE TABLE IF NOT EXISTS customer (id bigint PRIMARY KEY, wp_user_id bigint, first_name text NOT NULL DEFAULT '', last_name text DEFAULT '', email text, phone text, address text, is_guest boolean NOT NULL DEFAULT false, created_at timestamptz DEFAULT now())", "CREATE SEQUENCE IF NOT EXISTS customer_id_seq", "ALTER TABLE customer ALTER COLUMN id SET DEFAULT nextval('customer_id_seq')", "SELECT setval('customer_id_seq', GREATEST(1, (SELECT coalesce(max(id),0) FROM customer)))", "CREATE TABLE IF NOT EXISTS sales_discount (id bigint PRIMARY KEY, name text NOT NULL, percentage numeric(5,2) NOT NULL DEFAULT 0, is_active boolean NOT NULL DEFAULT true, manual_active boolean NOT NULL DEFAULT false, discount_type text DEFAULT 'manual', description text, bubble_text text, bubble_color text, start_at timestamptz, end_at timestamptz, created_at timestamptz DEFAULT now())", "CREATE TABLE IF NOT EXISTS sales_setting (setting_key text PRIMARY KEY, setting_value text, is_active boolean NOT NULL DEFAULT true, updated_at timestamptz DEFAULT now())", # a Guest customer always available at the counter "INSERT INTO customer (id, first_name, last_name, email, is_guest) VALUES (0, 'Guest', 'Sale', NULL, true) ON CONFLICT (id) DO NOTHING", # POS fields on the migrated sales tables "ALTER TABLE sales ADD COLUMN IF NOT EXISTS subtotal numeric(10,2)", "ALTER TABLE sales ADD COLUMN IF NOT EXISTS discount_amount numeric(10,2) DEFAULT 0", "ALTER TABLE sales ADD COLUMN IF NOT EXISTS payment_status text DEFAULT 'paid'", "ALTER TABLE sales ADD COLUMN IF NOT EXISTS amount_paid numeric(10,2) DEFAULT 0", "ALTER TABLE sales ADD COLUMN IF NOT EXISTS hold_expires_at timestamptz", "ALTER TABLE sales ADD COLUMN IF NOT EXISTS split_payments jsonb", "ALTER TABLE sales ADD COLUMN IF NOT EXISTS trade_in_credit numeric(10,2) DEFAULT 0", "ALTER TABLE sale_items ADD COLUMN IF NOT EXISTS discount_amount numeric(10,2) DEFAULT 0", "ALTER TABLE sale_items ADD COLUMN IF NOT EXISTS original_price numeric(10,2)", # auto-id for new POS rows (migrated tables came without sequences) "CREATE SEQUENCE IF NOT EXISTS sales_id_seq", "ALTER TABLE sales ALTER COLUMN id SET DEFAULT nextval('sales_id_seq')", "SELECT setval('sales_id_seq', GREATEST(1, (SELECT coalesce(max(id),0) FROM sales)))", "CREATE SEQUENCE IF NOT EXISTS sale_items_id_seq", "ALTER TABLE sale_items ALTER COLUMN id SET DEFAULT nextval('sale_items_id_seq')", "SELECT setval('sale_items_id_seq', GREATEST(1, (SELECT coalesce(max(id),0) FROM sale_items)))", ] @app.on_event("startup") async def _ensure_tables(): # lazy migrations on deploy (store_config + POS columns/sequences) async with engine.begin() as conn: for _stmt in _STARTUP_DDL: await conn.execute(_sqltext(_stmt)) @app.get("/healthz") async def healthz(): return {"ok": True, "service": "recordgod", "version": __version__} _ROOT = pathlib.Path(__file__).resolve().parent.parent # Pretty URLs: /builder /shop /admin /dash → the .html pages (registered before the "/" mount wins). def _page(filename): async def _serve(): return FileResponse(_ROOT / "site" / filename) return _serve for _stub in ("shop", "builder", "admin", "dash", "pos", "search"): app.add_api_route(f"/{_stub}", _page(_stub + ".html"), methods=["GET"], include_in_schema=False) # Mounts come last so the API routes + /healthz match first; "/" serves the landing/dash site. if (_ROOT / "webstore").is_dir(): app.mount("/store", StaticFiles(directory=str(_ROOT / "webstore"), html=True), name="store") if (_ROOT / "site").is_dir(): app.mount("/", StaticFiles(directory=str(_ROOT / "site"), html=True), name="site")