import uuid import enum from datetime import datetime, timezone from sqlalchemy import String, Boolean, DateTime, Enum as SAEnum, ForeignKey, Text, Numeric, Integer from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database import Base class SmsJobStatus(str, enum.Enum): pending = "pending" processing = "processing" sent = "sent" failed = "failed" cancelled = "cancelled" class SmsCreditTx(str, enum.Enum): topup = "topup" deduct = "deduct" refund = "refund" adjustment = "adjustment" class SmsJob(Base): __tablename__ = "sms_jobs" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) school_id: Mapped[str] = mapped_column(String(36), ForeignKey("schools.id", ondelete="CASCADE"), nullable=False, index=True) recipient_phone: Mapped[str] = mapped_column(String(20), nullable=False) message: Mapped[str] = mapped_column(Text, nullable=False) sender_name: Mapped[str] = mapped_column(String(11), nullable=False) status: Mapped[SmsJobStatus] = mapped_column(SAEnum(SmsJobStatus), default=SmsJobStatus.pending, nullable=False, index=True) trigger: Mapped[str | None] = mapped_column(String(50), nullable=True) # "absent", "late", "manual" semaphore_message_id: Mapped[str | None] = mapped_column(String(100), nullable=True) error_message: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) sent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) retry_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) school: Mapped["School"] = relationship("School", back_populates="sms_jobs") class SmsCreditLedger(Base): __tablename__ = "sms_credit_ledger" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) school_id: Mapped[str] = mapped_column(String(36), ForeignKey("schools.id", ondelete="CASCADE"), nullable=False, index=True) tx_type: Mapped[SmsCreditTx] = mapped_column(SAEnum(SmsCreditTx), nullable=False) amount: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False) balance_after: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) reference_id: Mapped[str | None] = mapped_column(String(36), nullable=True) # invoice_id or sms_job_id created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) created_by: Mapped[str | None] = mapped_column(String(36), nullable=True) # hub_user_id