- Embeddable vanilla-JS chat widget (one script tag + tenant public key):
WebSocket, resumable sessions (24h localStorage token), honeypot + per-IP
and per-message rate limits, Bosnian UI (§7)
- Session auth (bcrypt + signed cookies), owner accounts + super-admins,
admin impersonation ('otvori kao salon', §11)
- Owner dashboard (§10, Bosnian): requests with same actions as email,
call/chat history with transcripts, usage bar; settings for profile,
working hours (manual + LLM paste-to-parse), services CRUD + 'Zalijepi
cjenovnik' LLM import with review step, scheduling (Google OAuth free/busy
read-only + calendar mappings + buffers, partner status), telephony
(forwarding MMI codes per operator, ring group, worker SIP creds + QR),
agent voice/price-mode/notes + widget snippet, notifications/SMS templates
- Super-admin panel (§11): tenant CRUD incl. plan/minutes/paid-until,
partner API config (encrypted secrets), number/SIM registry + assignment,
connectivity test (live availability + dry-run push), versioned prompt
template editor with publish/rollback, playground, usage overview, health
- Fixes: WAL + busy_timeout for sqlite tests, no awaits in WS disconnect
path (deadlock), template publish autoflush bug
- 76 tests green (incl. widget WS e2e booking flow); dashboard, widget and
admin verified live in a browser against Postgres
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
112 lines
3.7 KiB
Python
112 lines
3.7 KiB
Python
"""Seed demo data: the fictional 'Salon Merima' (Appendix A) + super-admin.
|
|
|
|
Usage: python -m gogo.seed [--partner]
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import sys
|
|
|
|
from sqlalchemy import select
|
|
|
|
from gogo.auth import hash_password
|
|
from gogo.crypto import encrypt
|
|
from gogo.db import Base, get_engine, get_sessionmaker
|
|
from gogo.models import Admin, ProviderConfig, Service, Tenant, User
|
|
|
|
MERIMA_HOURS = {
|
|
"mon": [["09:00", "18:00"]],
|
|
"tue": [["09:00", "18:00"]],
|
|
"wed": [["09:00", "18:00"]],
|
|
"thu": [["09:00", "18:00"]],
|
|
"fri": [["09:00", "18:00"]],
|
|
"sat": [["09:00", "14:00"]],
|
|
"sun": [],
|
|
}
|
|
|
|
MERIMA_SERVICES = [
|
|
# name, duration, price_min, price_max, home_visit, note
|
|
("Šišanje i feniranje", 45, 25, 35, False, ""),
|
|
("Farbanje cijele dužine", 90, 60, 90, False, "cijena zavisi od dužine kose"),
|
|
("Pramenovi", 120, 80, 120, False, ""),
|
|
("Keratinski tretman", 90, 70, 100, False, "za oštećenu kosu"),
|
|
("Dubinska njega", 45, 30, 40, False, "za oštećenu kosu"),
|
|
("Manikir", 45, 20, 25, False, ""),
|
|
("Gel nokti", 90, 50, 70, False, ""),
|
|
("Pedikir", 60, 30, 30, True, "moguć dolazak na kućnu adresu"),
|
|
("Depilacija nogu", 30, 15, 20, False, ""),
|
|
]
|
|
|
|
|
|
async def seed(partner: bool = False) -> None:
|
|
engine = get_engine()
|
|
async with engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
|
|
async with get_sessionmaker()() as session:
|
|
existing = (
|
|
await session.execute(select(Tenant).where(Tenant.slug == "salon-merima"))
|
|
).scalar_one_or_none()
|
|
if existing:
|
|
print("salon-merima already seeded")
|
|
return
|
|
|
|
tenant = Tenant(
|
|
name="Salon Merima",
|
|
slug="salon-merima",
|
|
address="Ulica Veselina Masleše 12",
|
|
city="Banja Luka",
|
|
phone="+38751123456",
|
|
working_hours=MERIMA_HOURS,
|
|
scheduling_provider="partner_api" if partner else "mock",
|
|
notify_emails=["vlasnica@salon-merima.ba"],
|
|
agent_notes="Parking iza zgrade. Ne primamo djecu ispod 7 godina.",
|
|
price_mode="range",
|
|
)
|
|
session.add(tenant)
|
|
await session.flush()
|
|
|
|
for name, dur, pmin, pmax, home, note in MERIMA_SERVICES:
|
|
session.add(
|
|
Service(
|
|
tenant_id=tenant.id, name=name, duration_min=dur,
|
|
price_min=pmin, price_max=pmax, home_visit=home, agent_note=note,
|
|
)
|
|
)
|
|
|
|
if partner:
|
|
session.add(
|
|
ProviderConfig(
|
|
tenant_id=tenant.id,
|
|
provider_type="partner_api",
|
|
config={
|
|
"base_url": "http://localhost:9100",
|
|
"api_key_encrypted": encrypt("test-partner-key"),
|
|
"webhook_secret_encrypted": encrypt("test-webhook-secret"),
|
|
"catalog_sync": False,
|
|
"email_to_owner": True,
|
|
"polling_fallback": False,
|
|
},
|
|
)
|
|
)
|
|
|
|
session.add(
|
|
User(
|
|
tenant_id=tenant.id,
|
|
email="vlasnica@salon-merima.ba",
|
|
password_hash=hash_password("merima123"),
|
|
)
|
|
)
|
|
session.add(
|
|
Admin(email="admin@gogotelefon.ba", password_hash=hash_password("admin123"))
|
|
)
|
|
await session.commit()
|
|
print(f"seeded tenant {tenant.slug} ({tenant.id})")
|
|
print("owner login: vlasnica@salon-merima.ba / merima123")
|
|
print("super-admin: admin@gogotelefon.ba / admin123")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(seed(partner="--partner" in sys.argv))
|