"""Phase 6.2 sales engine DTOs.""" from __future__ import annotations from datetime import date, datetime from decimal import Decimal from uuid import UUID from pydantic import BaseModel, Field from app.models.types import ( ContactRoleType, ForecastKind, ForecastPeriod, GoalMetric, GoalScope, OpportunityStatus, PlaybookAssignmentStatus, PlaybookStepKind, TargetPeriod, WinLossOutcome, ) from app.schemas.common import ORMBase # ── Pipeline / Stage ───────────────────────────────────────────────────────── class PipelineCreate(BaseModel): name: str = Field(max_length=255) code: str = Field(max_length=50) description: str | None = None sort_order: int = 0 is_default: bool = False is_active: bool = True status: str = "active" class PipelineUpdate(BaseModel): name: str | None = Field(default=None, max_length=255) description: str | None = None sort_order: int | None = None is_default: bool | None = None is_active: bool | None = None status: str | None = None class PipelineRead(ORMBase): id: UUID tenant_id: UUID name: str code: str description: str | None sort_order: int is_default: bool is_active: bool status: str created_at: datetime updated_at: datetime class PipelineStageCreate(BaseModel): pipeline_id: UUID name: str = Field(max_length=255) code: str = Field(max_length=50) position: int = 0 win_probability: int = Field(default=0, ge=0, le=100) color: str | None = Field(default=None, max_length=20) is_start_stage: bool = False is_won_stage: bool = False is_lost_stage: bool = False is_closed_stage: bool = False required_fields: list[str] | None = None exit_validation: dict | None = None is_active: bool = True class PipelineStageUpdate(BaseModel): name: str | None = Field(default=None, max_length=255) position: int | None = None win_probability: int | None = Field(default=None, ge=0, le=100) color: str | None = Field(default=None, max_length=20) is_start_stage: bool | None = None is_won_stage: bool | None = None is_lost_stage: bool | None = None is_closed_stage: bool | None = None required_fields: list[str] | None = None exit_validation: dict | None = None is_active: bool | None = None class PipelineStageRead(ORMBase): id: UUID tenant_id: UUID pipeline_id: UUID name: str code: str position: int win_probability: int color: str | None is_start_stage: bool is_won_stage: bool is_lost_stage: bool is_closed_stage: bool required_fields: list | None exit_validation: dict | None is_active: bool created_at: datetime updated_at: datetime # ── Opportunity ────────────────────────────────────────────────────────────── class OpportunityCreate(BaseModel): name: str | None = Field(default=None, max_length=255) title: str | None = Field(default=None, max_length=255) opportunity_number: str | None = Field(default=None, max_length=50) pipeline_id: UUID stage_id: UUID amount: Decimal = Decimal("0") expected_revenue: Decimal | None = None currency_code: str = Field(default="IRR", max_length=3) probability: int = Field(default=0, ge=0, le=100) priority: str = "medium" expected_close_date: date | None = None organization_id: UUID | None = None contact_id: UUID | None = None lead_id: UUID | None = None owner_user_id: str | None = Field(default=None, max_length=100) description: str | None = None class OpportunityUpdate(BaseModel): name: str | None = Field(default=None, max_length=255) title: str | None = Field(default=None, max_length=255) stage_id: UUID | None = None amount: Decimal | None = None expected_revenue: Decimal | None = None currency_code: str | None = Field(default=None, max_length=3) probability: int | None = Field(default=None, ge=0, le=100) priority: str | None = None expected_close_date: date | None = None organization_id: UUID | None = None contact_id: UUID | None = None owner_user_id: str | None = Field(default=None, max_length=100) description: str | None = None class OpportunityRead(ORMBase): id: UUID tenant_id: UUID opportunity_number: str name: str title: str pipeline_id: UUID stage_id: UUID status: OpportunityStatus amount: Decimal expected_revenue: Decimal currency_code: str probability: int priority: str expected_close_date: date | None organization_id: UUID | None contact_id: UUID | None lead_id: UUID | None owner_user_id: str | None description: str | None won_at: datetime | None lost_at: datetime | None is_deleted: bool created_by: str | None updated_by: str | None created_at: datetime updated_at: datetime class OpportunityStageChangeRequest(BaseModel): stage_id: UUID reason: str | None = None class OpportunityWinRequest(BaseModel): reason_id: UUID | None = None category_id: UUID | None = None sub_category: str | None = None root_cause: str | None = None competitor_name: str | None = None price_factor: bool = False feature_gap: bool = False timing_factor: bool = False relationship_factor: bool = False approval_factor: bool = False custom_reason: str | None = None class OpportunityLoseRequest(BaseModel): reason_id: UUID | None = None category_id: UUID | None = None sub_category: str | None = None root_cause: str | None = None competitor_name: str | None = None price_factor: bool = False feature_gap: bool = False timing_factor: bool = False relationship_factor: bool = False approval_factor: bool = False custom_reason: str | None = None class StageHistoryRead(ORMBase): id: UUID tenant_id: UUID opportunity_id: UUID previous_stage_id: UUID | None next_stage_id: UUID changed_by: str | None changed_at: datetime duration_seconds: int | None reason: str | None created_at: datetime # ── Products / Contacts / Competitors ──────────────────────────────────────── class OpportunityProductCreate(BaseModel): opportunity_id: UUID product_ref_id: str = Field(max_length=100) product_name: str | None = Field(default=None, max_length=255) quantity: Decimal = Decimal("1") unit_price: Decimal = Decimal("0") discount_percent: Decimal = Decimal("0") class OpportunityProductRead(ORMBase): id: UUID tenant_id: UUID opportunity_id: UUID product_ref_id: str product_name: str | None quantity: Decimal unit_price: Decimal discount_percent: Decimal expected_revenue: Decimal created_at: datetime class OpportunityContactCreate(BaseModel): opportunity_id: UUID contact_id: UUID role: ContactRoleType = ContactRoleType.INFLUENCER custom_role_label: str | None = Field(default=None, max_length=100) is_primary: bool = False class OpportunityContactRead(ORMBase): id: UUID tenant_id: UUID opportunity_id: UUID contact_id: UUID role: ContactRoleType custom_role_label: str | None is_primary: bool created_at: datetime class OpportunityCompetitorCreate(BaseModel): opportunity_id: UUID competitor_name: str = Field(max_length=255) strength: str | None = None weakness: str | None = None notes: str | None = None win_probability: int = Field(default=0, ge=0, le=100) class OpportunityCompetitorRead(ORMBase): id: UUID tenant_id: UUID opportunity_id: UUID competitor_name: str strength: str | None weakness: str | None notes: str | None win_probability: int created_at: datetime # ── Playbooks ──────────────────────────────────────────────────────────────── class PlaybookCreate(BaseModel): name: str = Field(max_length=255) code: str = Field(max_length=50) description: str | None = None version: int = 1 pipeline_id: UUID | None = None is_active: bool = True class PlaybookRead(ORMBase): id: UUID tenant_id: UUID name: str code: str description: str | None version: int pipeline_id: UUID | None is_active: bool is_deleted: bool created_at: datetime class PlaybookStepCreate(BaseModel): playbook_id: UUID title: str = Field(max_length=255) step_kind: PlaybookStepKind position: int = 0 is_required: bool = False checklist_items: list[str] | None = None template_body: str | None = None class PlaybookStepRead(ORMBase): id: UUID tenant_id: UUID playbook_id: UUID title: str step_kind: PlaybookStepKind position: int is_required: bool checklist_items: list | None template_body: str | None created_at: datetime class PlaybookAssignRequest(BaseModel): playbook_id: UUID opportunity_id: UUID | None = None pipeline_id: UUID | None = None class PlaybookAssignmentRead(ORMBase): id: UUID tenant_id: UUID playbook_id: UUID opportunity_id: UUID | None pipeline_id: UUID | None status: PlaybookAssignmentStatus assigned_by: str | None completed_steps: list | None completion_percent: int completed_at: datetime | None created_at: datetime class PlaybookCompleteStepRequest(BaseModel): step_id: UUID # ── Win/Loss / Forecast / Goals / Targets ──────────────────────────────────── class WinLossCategoryCreate(BaseModel): code: str = Field(max_length=50) name: str = Field(max_length=150) parent_category_id: UUID | None = None is_active: bool = True class WinLossCategoryRead(ORMBase): id: UUID tenant_id: UUID code: str name: str parent_category_id: UUID | None is_active: bool created_at: datetime class WinLossReasonCreate(BaseModel): code: str = Field(max_length=50) name: str = Field(max_length=150) outcome: WinLossOutcome category_id: UUID | None = None is_active: bool = True class WinLossReasonRead(ORMBase): id: UUID tenant_id: UUID code: str name: str outcome: WinLossOutcome category_id: UUID | None is_active: bool created_at: datetime class OpportunityWinLossRead(ORMBase): id: UUID tenant_id: UUID opportunity_id: UUID outcome: WinLossOutcome reason_id: UUID | None category_id: UUID | None sub_category: str | None root_cause: str | None competitor_name: str | None price_factor: bool feature_gap: bool timing_factor: bool relationship_factor: bool approval_factor: bool custom_reason: str | None recorded_by: str | None recorded_at: datetime is_locked: bool created_at: datetime class ForecastComputeRequest(BaseModel): name: str = Field(max_length=255) kind: ForecastKind = ForecastKind.WEIGHTED period: ForecastPeriod period_start: date period_end: date pipeline_id: UUID | None = None owner_user_id: str | None = None currency_code: str = "IRR" class ForecastRead(ORMBase): id: UUID tenant_id: UUID name: str kind: ForecastKind period: ForecastPeriod period_start: date period_end: date pipeline_id: UUID | None owner_user_id: str | None pipeline_amount: Decimal weighted_amount: Decimal committed_amount: Decimal currency_code: str opportunity_count: int created_at: datetime class GoalCreate(BaseModel): name: str = Field(max_length=255) scope: GoalScope metric: GoalMetric owner_user_id: str | None = None team_key: str | None = None department_key: str | None = None target_value: Decimal period_start: date period_end: date is_active: bool = True class GoalRead(ORMBase): id: UUID tenant_id: UUID name: str scope: GoalScope metric: GoalMetric owner_user_id: str | None team_key: str | None department_key: str | None target_value: Decimal period_start: date period_end: date is_active: bool created_at: datetime class TargetCreate(BaseModel): name: str = Field(max_length=255) period: TargetPeriod period_start: date period_end: date goal_id: UUID | None = None owner_user_id: str | None = None target_value: Decimal actual_value: Decimal = Decimal("0") class TargetRead(ORMBase): id: UUID tenant_id: UUID name: str period: TargetPeriod period_start: date period_end: date goal_id: UUID | None owner_user_id: str | None target_value: Decimal actual_value: Decimal achievement_percent: Decimal created_at: datetime class SalesAuditRead(ORMBase): id: UUID tenant_id: UUID entity_type: str entity_id: UUID action: str actor_user_id: str | None changes: dict | None message: str | None created_at: datetime class SalesProcessCreate(BaseModel): name: str = Field(max_length=255) code: str = Field(max_length=50) description: str | None = None pipeline_id: UUID | None = None class SalesProcessRead(ORMBase): id: UUID tenant_id: UUID name: str code: str description: str | None pipeline_id: UUID | None is_active: bool created_at: datetime updated_at: datetime