Files
HermesMessages/backend/alembic/versions/0001_initial.py
Hanzo_dev 798bd14312 feat: initial commit — HermesMessages SaaS platform
Backend (FastAPI + Python 3.12):
- Multi-tenant auth with JWT: login, register, refresh, Meta OAuth
- Business & BusinessConfig management
- WhatsApp webhook with HMAC signature verification
- Bot engine powered by Claude AI
- Calendar availability with Redis caching
- Reservations CRUD with status management
- Dashboard analytics (stats, agenda, peak hours)
- Billing & plan management
- Admin panel with platform-wide stats
- Async bcrypt via asyncio.to_thread
- IntegrityError handling for concurrent registration race conditions

Frontend (React 18 + Vite + Tailwind CSS):
- Multi-step guided registration form with helper text on every field
- Login page with show/hide password toggle
- Protected routes with AuthContext
- Dashboard with stats cards, bar chart, and daily agenda
- Reservations list with search, filters, and inline status actions
- Calendar with weekly view, slot availability, and date blocking
- Config page: business info, schedules, bot personality
- Billing page with plan comparison and usage bar

Design system:
- Bricolage Grotesque + DM Sans typography
- Emerald primary palette with semantic color tokens
- scale(0.97) button press feedback, ease-out animations
- Skeleton loaders, stagger animations, prefers-reduced-motion support
- Accessible: aria-labels, visible focus rings, 4.5:1 contrast

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:49:41 -05:00

156 lines
5.8 KiB
Python

"""initial schema
Revision ID: 0001
Revises:
Create Date: 2026-04-27
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision = "0001"
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# --- Enums ---
business_status = postgresql.ENUM("trial", "active", "suspended", name="business_status")
business_plan = postgresql.ENUM("free", "basic", "pro", name="business_plan")
user_role = postgresql.ENUM("owner", "admin", name="user_role")
assistant_tone = postgresql.ENUM("formal", "casual", name="assistant_tone")
reservation_status = postgresql.ENUM(
"pending", "confirmed", "cancelled", "no_show", name="reservation_status"
)
reservation_source = postgresql.ENUM("whatsapp", "manual", name="reservation_source")
for enum in [business_status, business_plan, user_role, assistant_tone,
reservation_status, reservation_source]:
enum.create(op.get_bind(), checkfirst=True)
# --- businesses ---
op.create_table(
"businesses",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("name", sa.String(), nullable=False),
sa.Column("type", sa.String(), nullable=False),
sa.Column("timezone", sa.String(), nullable=False, server_default="UTC"),
sa.Column(
"status",
sa.Enum("trial", "active", "suspended", name="business_status"),
nullable=False,
server_default="trial",
),
sa.Column(
"plan",
sa.Enum("free", "basic", "pro", name="business_plan"),
nullable=False,
server_default="free",
),
sa.Column("meta_business_id", sa.String(), nullable=True),
sa.Column("whatsapp_phone_number_id", sa.String(), nullable=True, unique=True),
sa.Column("whatsapp_access_token", sa.String(), nullable=True),
sa.Column("created_at", sa.Date(), server_default=sa.func.current_date()),
)
op.create_index("ix_businesses_id", "businesses", ["id"])
# --- users ---
op.create_table(
"users",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column(
"business_id",
sa.Integer(),
sa.ForeignKey("businesses.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("email", sa.String(), nullable=False, unique=True),
sa.Column("hashed_password", sa.String(), nullable=True),
sa.Column("meta_user_id", sa.String(), nullable=True, unique=True),
sa.Column(
"role",
sa.Enum("owner", "admin", name="user_role"),
nullable=False,
server_default="owner",
),
)
op.create_index("ix_users_id", "users", ["id"])
op.create_index("ix_users_email", "users", ["email"])
# --- business_configs ---
op.create_table(
"business_configs",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column(
"business_id",
sa.Integer(),
sa.ForeignKey("businesses.id", ondelete="CASCADE"),
nullable=False,
unique=True,
),
sa.Column("open_days", postgresql.ARRAY(sa.Integer()), nullable=False, server_default="{}"),
sa.Column("open_time", sa.Time(), nullable=False, server_default="09:00"),
sa.Column("close_time", sa.Time(), nullable=False, server_default="18:00"),
sa.Column("slot_duration", sa.Integer(), nullable=False, server_default="60"),
sa.Column("max_per_slot", sa.Integer(), nullable=False, server_default="1"),
sa.Column("blocked_dates", postgresql.ARRAY(sa.Date()), nullable=False, server_default="{}"),
sa.Column("assistant_name", sa.String(), nullable=False, server_default="Hermes"),
sa.Column(
"tone",
sa.Enum("formal", "casual", name="assistant_tone"),
nullable=False,
server_default="formal",
),
sa.Column("welcome_message", sa.String(), nullable=True),
)
op.create_index("ix_business_configs_id", "business_configs", ["id"])
# --- reservations ---
op.create_table(
"reservations",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column(
"business_id",
sa.Integer(),
sa.ForeignKey("businesses.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("client_name", sa.String(), nullable=False),
sa.Column("client_phone", sa.String(), nullable=False),
sa.Column("date", sa.Date(), nullable=False),
sa.Column("time_start", sa.Time(), nullable=False),
sa.Column("time_end", sa.Time(), nullable=False),
sa.Column("party_size", sa.Integer(), nullable=False, server_default="1"),
sa.Column(
"status",
sa.Enum("pending", "confirmed", "cancelled", "no_show", name="reservation_status"),
nullable=False,
server_default="pending",
),
sa.Column(
"source",
sa.Enum("whatsapp", "manual", name="reservation_source"),
nullable=False,
server_default="manual",
),
sa.Column("notes", sa.String(), nullable=True),
sa.Column("created_at", sa.Date(), server_default=sa.func.current_date()),
)
op.create_index("ix_reservations_id", "reservations", ["id"])
op.create_index("ix_reservations_date", "reservations", ["date"])
def downgrade() -> None:
op.drop_table("reservations")
op.drop_table("business_configs")
op.drop_table("users")
op.drop_table("businesses")
for name in [
"reservation_source", "reservation_status", "assistant_tone",
"user_role", "business_plan", "business_status",
]:
postgresql.ENUM(name=name).drop(op.get_bind(), checkfirst=True)