scaffold: schema, compose, contract
This commit is contained in:
85
CONTRACT.md
Normal file
85
CONTRACT.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# Polaris — Reseller Platform · Module Contract (agents read this)
|
||||||
|
|
||||||
|
Single source of truth for the 3 parallel build agents. Do NOT drift from these
|
||||||
|
interfaces — the other agents depend on them exactly as written.
|
||||||
|
|
||||||
|
## Stack (locked)
|
||||||
|
- Backend: **FastAPI** (Python 3.12) in `/backend`, SQLAlchemy 2.0 ORM, Pydantic v2
|
||||||
|
- DB: **PostgreSQL 16** (schema in `db/schema.sql` — DO NOT modify it, build against it)
|
||||||
|
- Queue: **Celery** + **Redis** (`redis://redis:6379/0`)
|
||||||
|
- Frontend: **React 18 + Vite + TypeScript + Tailwind** in `/frontend`
|
||||||
|
- Deploy: Docker Compose (root `docker-compose.yml` already has postgres+redis)
|
||||||
|
|
||||||
|
## Service URLs (inside Docker network)
|
||||||
|
- postgres: `postgres://reseller:${POSTGRES_PASSWORD}@postgres:5432/reseller`
|
||||||
|
- redis: `redis://redis:6379/0`
|
||||||
|
- api listens on `0.0.0.0:8000`
|
||||||
|
|
||||||
|
## BACKEND agent owns: /backend
|
||||||
|
- `app/main.py` — FastAPI app, CORS allow-all (MVP), mounts routers under `/api`
|
||||||
|
- `app/models.py` — SQLAlchemy models mirroring `db/schema.sql` EXACTLY (table + column names)
|
||||||
|
- `app/database.py` — engine from `DATABASE_URL` env, `SessionLocal`, `Base`
|
||||||
|
- `app/schemas.py` — Pydantic v2 request/response models
|
||||||
|
- `app/engines/pricing.py` — `calculate_price(cost, shipping, fees)` -> returns retail,
|
||||||
|
margin, profit using active `price_rules`; `estimate_net_profit(...)`; never below min_price
|
||||||
|
- `app/engines/suppliers/base.py` — `SupplierAdapter` ABC:
|
||||||
|
get_products / get_inventory / get_price / create_order / get_order_status / get_tracking / cancel_order
|
||||||
|
- `app/engines/suppliers/csv_adapter.py` — CSV feed adapter (reads supplier CSV: sku,title,cost,inventory,shipping_cost,shipping_time,category,image_url)
|
||||||
|
- `app/engines/suppliers/sample.py` — sample supplier seeded with ~25 realistic products ($50-$500, home/gadgets/office niche)
|
||||||
|
- `app/engines/importer.py` — import feed -> normalize -> validate -> upsert products/supplier_products; never auto-publish invalid
|
||||||
|
- `app/engines/order_router.py` — `select_supplier(product_id, qty)` highest-score eligible supplier; `route_order(order)`
|
||||||
|
- `app/engines/scoring.py` — supplier score = 40% price + 20% inventory + 15% speed + 10% fulfillment + 10% returns + 5% history
|
||||||
|
- `app/engines/inventory.py` — sync supplier_products inventory, mark 0/unavailable, alert flags
|
||||||
|
- `app/tasks.py` — Celery: sync_inventory (every 5min), sync_tracking, recalc_prices
|
||||||
|
- `app/routers/` — products.py, suppliers.py, orders.py, customers.py, admin.py, analytics.py, health.py
|
||||||
|
- `alembic/` — optional; MVP may `Base.metadata.create_all()` instead (schema.sql is canonical)
|
||||||
|
|
||||||
|
## API endpoints (frontend depends on these — do not rename)
|
||||||
|
- `GET /api/health` -> {status, version}
|
||||||
|
- `GET /api/products` (filters: ?status=&category=&search=&limit=&offset=)
|
||||||
|
- `GET /api/products/{id}`
|
||||||
|
- `POST /api/products/{id}/publish`
|
||||||
|
- `POST /api/products/{id}/pause`
|
||||||
|
- `GET /api/products/{id}/price-history`
|
||||||
|
- `POST /api/products/import` (body: {supplier_id, feed_type, data_or_url})
|
||||||
|
- `GET /api/suppliers` / `POST /api/suppliers`
|
||||||
|
- `GET /api/suppliers/{id}/performance`
|
||||||
|
- `POST /api/orders` (body: {customer_email, items:[{product_id,qty}]}) -> creates+routes order
|
||||||
|
- `GET /api/orders` / `GET /api/orders/{id}`
|
||||||
|
- `POST /api/orders/{id}/tracking` (simulate supplier tracking push)
|
||||||
|
- `GET /api/analytics/summary` -> today revenue/orders/profit/margin/aov/conversion
|
||||||
|
- `GET /api/analytics/top-products`
|
||||||
|
- `GET /api/analytics/supplier-performance`
|
||||||
|
- `POST /api/auth/login` (admin, simple: env ADMIN_EMAIL/ADMIN_PASSWORD_HASH, returns JWT)
|
||||||
|
- `POST /api/auth/register` (customer)
|
||||||
|
|
||||||
|
## FRONTEND agent owns: /frontend
|
||||||
|
React+Vite+TS+Tailwind. Two surfaces, shared API client (`src/lib/api.ts`):
|
||||||
|
- `/` storefront: home, shop grid, category filter, product page, cart, checkout (mock
|
||||||
|
Stripe/BTCPay button -> creates order), order-tracking page. ORIGINAL brand "Polaris".
|
||||||
|
Dark premium aesthetic. BMAC footer link https://buymeacoffee.com/r26xrthzttg
|
||||||
|
- `/admin` dashboard: today KPI cards (revenue/orders/gross+net profit/margin/aov/refunds),
|
||||||
|
top products table, supplier performance table, import button, product lifecycle view,
|
||||||
|
order list with status + tracking. Cache-Control no-store on all API fetches.
|
||||||
|
- Env: `VITE_API_URL=/api` (nginx proxies).
|
||||||
|
|
||||||
|
## OPS agent owns: root deploy + docs
|
||||||
|
- Extend `docker-compose.yml` with: api, worker, frontend, nginx services
|
||||||
|
- `backend/Dockerfile`, `frontend/Dockerfile`, `nginx/nginx.conf` (proxy /api->api:8000, /->frontend)
|
||||||
|
- `.env.example` (POSTGRES_PASSWORD, DATABASE_URL, REDIS_URL, SECRET_KEY, ADMIN_EMAIL, ADMIN_PASSWORD, BTCPAY_URL/KEY, STRIPE_KEY)
|
||||||
|
- `README.md` (deploy steps), `docs/api.md` (endpoint reference), `tests/` (pytest smoke tests)
|
||||||
|
- systemd unit `polaris.service` wrapping `docker compose up -d`, enabled+started
|
||||||
|
|
||||||
|
## Verification (the parent re-runs these — every agent must self-verify first)
|
||||||
|
1. `docker compose up -d` clean, all containers healthy
|
||||||
|
2. seed sample supplier + import -> products appear in GET /api/products
|
||||||
|
3. pricing returns sensible margin for a $80 cost item
|
||||||
|
4. POST /api/orders creates order, routes to supplier, records profit
|
||||||
|
5. frontend `/` and `/admin` render with live API data
|
||||||
|
6. README deploy steps reproducible from scratch
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
- Secrets ONLY via env vars, never hard-coded.
|
||||||
|
- Backup any existing file before overwriting (cp x x.bak-<date>).
|
||||||
|
- Never delete the parent's work; only add/extend.
|
||||||
|
- All code self-contained; report exact files changed + live curl proof.
|
||||||
148
db/schema.sql
Normal file
148
db/schema.sql
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
-- Polaris — canonical PostgreSQL schema (single source of truth)
|
||||||
|
-- Database: reseller User: reseller
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||||
|
|
||||||
|
-- ── SUPPLIERS ──────────────────────────────────────────────
|
||||||
|
CREATE TABLE suppliers (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
adapter_type TEXT NOT NULL, -- csv|xml|json|api|affiliate
|
||||||
|
api_endpoint TEXT,
|
||||||
|
account_id TEXT,
|
||||||
|
credentials_enc TEXT, -- encrypted secret ref (never raw)
|
||||||
|
fulfillment_caps JSONB DEFAULT '[]',
|
||||||
|
shipping_regions JSONB DEFAULT '[]',
|
||||||
|
reliability_score NUMERIC(5,2) DEFAULT 50.0,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active', -- active|paused|failed
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── PRODUCTS ───────────────────────────────────────────────
|
||||||
|
CREATE TABLE products (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
sku TEXT UNIQUE NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
category TEXT,
|
||||||
|
brand TEXT,
|
||||||
|
manufacturer TEXT,
|
||||||
|
upc_ean_gtin TEXT,
|
||||||
|
images JSONB DEFAULT '[]',
|
||||||
|
dimensions JSONB, -- {length,width,height,unit}
|
||||||
|
weight_grams NUMERIC(10,2),
|
||||||
|
cost NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||||
|
retail_price NUMERIC(12,2),
|
||||||
|
min_price NUMERIC(12,2),
|
||||||
|
max_price NUMERIC(12,2),
|
||||||
|
status TEXT NOT NULL DEFAULT 'discovered', -- discovered|analyzing|approved|imported|content_generated|price_calculated|quality_check|published|monitored|paused|sold
|
||||||
|
lifecycle JSONB DEFAULT '{}', -- opportunity score etc
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── SUPPLIER_PRODUCTS ──────────────────────────────────────
|
||||||
|
CREATE TABLE supplier_products (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
supplier_id UUID NOT NULL REFERENCES suppliers(id) ON DELETE CASCADE,
|
||||||
|
supplier_sku TEXT NOT NULL,
|
||||||
|
product_id UUID REFERENCES products(id) ON DELETE SET NULL,
|
||||||
|
supplier_cost NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||||
|
supplier_inventory INTEGER NOT NULL DEFAULT 0,
|
||||||
|
shipping_cost NUMERIC(12,2) DEFAULT 0,
|
||||||
|
shipping_time TEXT, -- e.g. '3-5 days'
|
||||||
|
last_updated TIMESTAMPTZ DEFAULT now(),
|
||||||
|
UNIQUE (supplier_id, supplier_sku)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── CUSTOMERS ──────────────────────────────────────────────
|
||||||
|
CREATE TABLE customers (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
email TEXT UNIQUE NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
shipping_addr JSONB,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── ORDERS ─────────────────────────────────────────────────
|
||||||
|
CREATE TABLE orders (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
order_number TEXT UNIQUE NOT NULL,
|
||||||
|
customer_id UUID REFERENCES customers(id),
|
||||||
|
items JSONB NOT NULL, -- [{product_id,qty,unit_price}]
|
||||||
|
retail_total NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||||
|
supplier_id UUID REFERENCES suppliers(id),
|
||||||
|
supplier_cost NUMERIC(12,2) DEFAULT 0,
|
||||||
|
shipping_cost NUMERIC(12,2) DEFAULT 0,
|
||||||
|
fees NUMERIC(12,2) DEFAULT 0,
|
||||||
|
profit NUMERIC(12,2) DEFAULT 0,
|
||||||
|
status TEXT NOT NULL DEFAULT 'new', -- new|paid|fraud_check|supplier_order|confirmed|shipped|delivered|cancelled|refunded
|
||||||
|
tracking TEXT,
|
||||||
|
carrier TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── PRICE HISTORY ──────────────────────────────────────────
|
||||||
|
CREATE TABLE price_history (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||||
|
price NUMERIC(12,2) NOT NULL,
|
||||||
|
reason TEXT, -- manual|auto|competitor|cost_change
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── INVENTORY HISTORY ──────────────────────────────────────
|
||||||
|
CREATE TABLE inventory_history (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
supplier_product_id UUID NOT NULL REFERENCES supplier_products(id) ON DELETE CASCADE,
|
||||||
|
inventory INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── SUPPLIER PERFORMANCE ───────────────────────────────────
|
||||||
|
CREATE TABLE supplier_performance (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
supplier_id UUID NOT NULL REFERENCES suppliers(id) ON DELETE CASCADE,
|
||||||
|
fulfillment_rate NUMERIC(5,2) DEFAULT 100,
|
||||||
|
avg_shipping_days NUMERIC(6,2),
|
||||||
|
cancellation_rate NUMERIC(5,2) DEFAULT 0,
|
||||||
|
stock_accuracy NUMERIC(5,2) DEFAULT 100,
|
||||||
|
return_rate NUMERIC(5,2) DEFAULT 0,
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── PRICE RULES ────────────────────────────────────────────
|
||||||
|
CREATE TABLE price_rules (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
min_cost NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||||
|
max_cost NUMERIC(12,2), -- null = no upper bound
|
||||||
|
markup_pct NUMERIC(6,2) NOT NULL,
|
||||||
|
min_margin_pct NUMERIC(6,2) DEFAULT 0,
|
||||||
|
active BOOLEAN DEFAULT true,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── AUDIT LOG ──────────────────────────────────────────────
|
||||||
|
CREATE TABLE audit_logs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
actor TEXT,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
entity TEXT,
|
||||||
|
entity_id TEXT,
|
||||||
|
detail JSONB,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- seed default price rules (cost -> markup)
|
||||||
|
INSERT INTO price_rules (min_cost, max_cost, markup_pct) VALUES
|
||||||
|
(0, 25, 50.00),
|
||||||
|
(25, 100, 35.00),
|
||||||
|
(100, 500, 25.00),
|
||||||
|
(500, NULL, 15.00);
|
||||||
|
|
||||||
|
CREATE INDEX idx_supplier_products_product ON supplier_products(product_id);
|
||||||
|
CREATE INDEX idx_orders_status ON orders(status);
|
||||||
|
CREATE INDEX idx_products_status ON products(status);
|
||||||
|
CREATE INDEX idx_price_history_product ON price_history(product_id);
|
||||||
30
docker-compose.yml
Normal file
30
docker-compose.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: reseller
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_DB: reseller
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
- ./db/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U reseller"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
Reference in New Issue
Block a user