feat: add Layer 2 - WhatsApp Core logic, API Gateway models/auth, Frontend core
WhatsApp Core: - SessionManager with Baileys integration for multi-account support - Express server with REST API and Socket.IO for real-time events - Session lifecycle management (create, disconnect, delete) - Message sending with support for text, image, document, audio, video API Gateway: - Database models: User, WhatsAppAccount, Contact, Conversation, Message - JWT authentication with access/refresh tokens - Auth endpoints: login, refresh, register, me - Pydantic schemas for request/response validation Frontend: - React 18 app structure with routing - Zustand auth store with persistence - API client with automatic token handling - Base CSS and TypeScript declarations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
4
services/api-gateway/app/models/__init__.py
Normal file
4
services/api-gateway/app/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from app.models.user import User
|
||||
from app.models.whatsapp import WhatsAppAccount, Contact, Conversation, Message
|
||||
|
||||
__all__ = ["User", "WhatsAppAccount", "Contact", "Conversation", "Message"]
|
||||
33
services/api-gateway/app/models/user.py
Normal file
33
services/api-gateway/app/models/user.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, String, Boolean, DateTime, Enum as SQLEnum
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
import enum
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class UserRole(str, enum.Enum):
|
||||
ADMIN = "admin"
|
||||
SUPERVISOR = "supervisor"
|
||||
AGENT = "agent"
|
||||
|
||||
|
||||
class UserStatus(str, enum.Enum):
|
||||
ONLINE = "online"
|
||||
OFFLINE = "offline"
|
||||
AWAY = "away"
|
||||
BUSY = "busy"
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
email = Column(String(255), unique=True, nullable=False, index=True)
|
||||
password_hash = Column(String(255), nullable=False)
|
||||
name = Column(String(100), nullable=False)
|
||||
role = Column(SQLEnum(UserRole), default=UserRole.AGENT, nullable=False)
|
||||
status = Column(SQLEnum(UserStatus), default=UserStatus.OFFLINE, nullable=False)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
111
services/api-gateway/app/models/whatsapp.py
Normal file
111
services/api-gateway/app/models/whatsapp.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, String, Boolean, DateTime, Text, Integer, ForeignKey, Enum as SQLEnum
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB, ARRAY
|
||||
from sqlalchemy.orm import relationship
|
||||
import enum
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class AccountStatus(str, enum.Enum):
|
||||
CONNECTING = "connecting"
|
||||
CONNECTED = "connected"
|
||||
DISCONNECTED = "disconnected"
|
||||
|
||||
|
||||
class ConversationStatus(str, enum.Enum):
|
||||
BOT = "bot"
|
||||
WAITING = "waiting"
|
||||
ACTIVE = "active"
|
||||
RESOLVED = "resolved"
|
||||
|
||||
|
||||
class MessageDirection(str, enum.Enum):
|
||||
INBOUND = "inbound"
|
||||
OUTBOUND = "outbound"
|
||||
|
||||
|
||||
class MessageType(str, enum.Enum):
|
||||
TEXT = "text"
|
||||
IMAGE = "image"
|
||||
AUDIO = "audio"
|
||||
VIDEO = "video"
|
||||
DOCUMENT = "document"
|
||||
LOCATION = "location"
|
||||
CONTACT = "contact"
|
||||
STICKER = "sticker"
|
||||
BUTTONS = "buttons"
|
||||
LIST = "list"
|
||||
|
||||
|
||||
class MessageStatus(str, enum.Enum):
|
||||
PENDING = "pending"
|
||||
SENT = "sent"
|
||||
DELIVERED = "delivered"
|
||||
READ = "read"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class WhatsAppAccount(Base):
|
||||
__tablename__ = "whatsapp_accounts"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
phone_number = Column(String(20), nullable=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
status = Column(SQLEnum(AccountStatus), default=AccountStatus.DISCONNECTED, nullable=False)
|
||||
session_data = Column(JSONB, nullable=True)
|
||||
qr_code = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
conversations = relationship("Conversation", back_populates="whatsapp_account")
|
||||
|
||||
|
||||
class Contact(Base):
|
||||
__tablename__ = "contacts"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
phone_number = Column(String(20), unique=True, nullable=False, index=True)
|
||||
name = Column(String(100), nullable=True)
|
||||
email = Column(String(255), nullable=True)
|
||||
company = Column(String(100), nullable=True)
|
||||
metadata = Column(JSONB, default=dict)
|
||||
tags = Column(ARRAY(String), default=list)
|
||||
odoo_partner_id = Column(Integer, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
conversations = relationship("Conversation", back_populates="contact")
|
||||
|
||||
|
||||
class Conversation(Base):
|
||||
__tablename__ = "conversations"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
whatsapp_account_id = Column(UUID(as_uuid=True), ForeignKey("whatsapp_accounts.id"), nullable=False)
|
||||
contact_id = Column(UUID(as_uuid=True), ForeignKey("contacts.id"), nullable=False)
|
||||
assigned_to = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
|
||||
status = Column(SQLEnum(ConversationStatus), default=ConversationStatus.BOT, nullable=False)
|
||||
last_message_at = Column(DateTime, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
whatsapp_account = relationship("WhatsAppAccount", back_populates="conversations")
|
||||
contact = relationship("Contact", back_populates="conversations")
|
||||
messages = relationship("Message", back_populates="conversation", order_by="Message.created_at")
|
||||
|
||||
|
||||
class Message(Base):
|
||||
__tablename__ = "messages"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
conversation_id = Column(UUID(as_uuid=True), ForeignKey("conversations.id"), nullable=False)
|
||||
whatsapp_message_id = Column(String(100), nullable=True)
|
||||
direction = Column(SQLEnum(MessageDirection), nullable=False)
|
||||
type = Column(SQLEnum(MessageType), default=MessageType.TEXT, nullable=False)
|
||||
content = Column(Text, nullable=True)
|
||||
media_url = Column(String(500), nullable=True)
|
||||
metadata = Column(JSONB, default=dict)
|
||||
sent_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
|
||||
is_internal_note = Column(Boolean, default=False, nullable=False)
|
||||
status = Column(SQLEnum(MessageStatus), default=MessageStatus.PENDING, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
conversation = relationship("Conversation", back_populates="messages")
|
||||
Reference in New Issue
Block a user