Modules give code names and boundaries; type hints make assumptions visible; domain models protect valid business state. A maintainable backend keeps the core meaning of a ticket independent from HTTP, database rows, and model-provider payloads so each outer technology can change without rewriting the business rules.
Create src/supportdesk_ai/domain/tickets.py:
frozen=True makes mutation explicit: resolve returns a new valid value. The organization identifier is part of the model because tenant ownership is a business fact, not an HTTP detail.
Suggested boundaries:
Verify types:
An HTTP creation request has no database-generated ID. A database row has persistence details. A model provider may use a strict classification schema. A domain ticket represents business truth. Reusing one giant model leaks fields, creates accidental write access, and tightly couples layers.
Provider schemas change more often than domain language. Keep ModelTicketClassification at the adapter boundary, translate it into an application command, then apply domain rules. The model never returns a fully trusted Ticket or chooses tenant ownership.
Add an assign(agent_id) method that rejects resolved tickets and returns a new ticket. Decide whether assignment belongs in the same aggregate and defend your answer.
Acceptance criteria: static checks pass; tests prove the resolved-state rule and immutability; no FastAPI import appears in domain/.