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>
This commit is contained in:
2026-04-28 09:49:41 -05:00
commit 798bd14312
95 changed files with 5836 additions and 0 deletions

View File

@ -0,0 +1,118 @@
from datetime import date, time
import pytest
from app.modules.reservations.schemas import ReservationCreate, ReservationUpdate, StatusUpdate
from app.modules.reservations.service import (
create_reservation,
delete_reservation,
get_reservation,
list_reservations,
update_status,
)
from tests.conftest import make_business, make_user
async def _create_test_reservation(db, redis_mock, business_id):
return await create_reservation(
db=db,
redis=redis_mock,
business_id=business_id,
data=ReservationCreate(
client_name="Ana García",
client_phone="5491112345678",
date=date(2026, 5, 5),
time_start=time(10, 0),
party_size=2,
source="manual",
),
)
async def test_create_reservation(db, redis_mock):
business = await make_business(db, name="Res Test")
r = await _create_test_reservation(db, redis_mock, business.id)
assert r.id is not None
assert r.client_name == "Ana García"
assert r.time_end == time(11, 0) # 10:00 + 60min slot
assert r.status == "pending"
assert r.source == "manual"
redis_mock.delete.assert_called_once() # caché invalidada
async def test_list_reservations_filter_by_date(db, redis_mock):
business = await make_business(db, name="List Test")
await _create_test_reservation(db, redis_mock, business.id)
results = await list_reservations(db, business.id, filter_date=date(2026, 5, 5))
assert len(results) >= 1
assert all(r.date == date(2026, 5, 5) for r in results)
async def test_list_reservations_filter_by_status(db, redis_mock):
business = await make_business(db, name="Status Test")
await _create_test_reservation(db, redis_mock, business.id)
results = await list_reservations(db, business.id, filter_status="pending")
assert all(r.status == "pending" for r in results)
async def test_get_reservation_not_found(db):
business = await make_business(db, name="404 Test")
from fastapi import HTTPException
with pytest.raises(HTTPException) as exc:
await get_reservation(db, business.id, 999999)
assert exc.value.status_code == 404
async def test_get_reservation_wrong_business(db, redis_mock):
b1 = await make_business(db, name="Business 1")
b2 = await make_business(db, name="Business 2")
r = await _create_test_reservation(db, redis_mock, b1.id)
from fastapi import HTTPException
with pytest.raises(HTTPException) as exc:
await get_reservation(db, b2.id, r.id)
assert exc.value.status_code == 404
async def test_update_status_to_confirmed(db, redis_mock):
business = await make_business(db, name="Confirm Test")
r = await _create_test_reservation(db, redis_mock, business.id)
updated = await update_status(db, redis_mock, business.id, r.id, "confirmed")
assert updated.status == "confirmed"
async def test_update_status_cancelled_invalidates_cache(db, redis_mock):
business = await make_business(db, name="Cancel Cache Test")
r = await _create_test_reservation(db, redis_mock, business.id)
redis_mock.delete.reset_mock()
await update_status(db, redis_mock, business.id, r.id, "cancelled")
redis_mock.delete.assert_called_once() # caché invalidada al cancelar
async def test_update_status_invalid_raises(db, redis_mock):
business = await make_business(db, name="Invalid Status Test")
r = await _create_test_reservation(db, redis_mock, business.id)
from fastapi import HTTPException
with pytest.raises(HTTPException) as exc:
await update_status(db, redis_mock, business.id, r.id, "inexistente")
assert exc.value.status_code == 422
async def test_delete_reservation(db, redis_mock):
business = await make_business(db, name="Delete Test")
r = await _create_test_reservation(db, redis_mock, business.id)
r_id = r.id
redis_mock.delete.reset_mock()
await delete_reservation(db, redis_mock, business.id, r_id)
redis_mock.delete.assert_called_once()
from fastapi import HTTPException
with pytest.raises(HTTPException):
await get_reservation(db, business.id, r_id)