| """ |
| Comprehensive tests for sales and service delivery models. |
| |
| Tests cover: |
| - Sales models (Lead, Deal, CommissionEntry, CallTranscript, FollowUpTask) |
| - Service delivery models (Contract, Project, Milestone, ProjectTask, Appointment) |
| - Cross-module relationships (Deal->Contract, Entity->Appointment) |
| - Budget tracking and guardrails |
| - AI enrichment features |
| - JSON field serialization |
| - Enum validation |
| - Workflow chains (deal->contract->project->milestone->task) |
| |
| Target: 80%+ line coverage for both sales.models and service_delivery.models |
| """ |
|
|
| import pytest |
| from datetime import datetime, timedelta, timezone |
| from sqlalchemy.orm import Session |
| from sqlalchemy import and_, or_ |
|
|
| from tests.factories.workspace_factory import WorkspaceFactory |
| from tests.factories.sales_factory import ( |
| LeadFactory, DealFactory, CommissionEntryFactory, |
| CallTranscriptFactory, FollowUpTaskFactory |
| ) |
| from tests.factories.service_factory import ( |
| ContractFactory, ProjectFactory, MilestoneFactory, |
| ProjectTaskFactory, AppointmentFactory |
| ) |
|
|
| from sales.models import ( |
| Lead, Deal, CommissionEntry, CallTranscript, FollowUpTask, |
| LeadStatus, DealStage, CommissionStatus, NegotiationState |
| ) |
| from service_delivery.models import ( |
| Contract, Project, Milestone, ProjectTask, Appointment, |
| ContractType, ProjectStatus, MilestoneStatus, BudgetStatus, AppointmentStatus |
| ) |
| from core.models import User, Workspace |
| from accounting.models import Entity |
|
|
|
|
| |
| |
| |
|
|
| class TestLeadModel: |
| """Test Lead model (CRM leads).""" |
|
|
| def test_lead_create_with_defaults(self, db_session: Session): |
| """Test Lead creation with required fields.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| lead = LeadFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
| db_session.refresh(lead) |
|
|
| assert lead.id is not None |
| assert lead.workspace_id == workspace.id |
| assert lead.email is not None |
| assert lead.status in [s.value for s in LeadStatus] |
|
|
| def test_lead_status_enum(self, db_session: Session): |
| """Test all LeadStatus enum values.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| statuses = [LeadStatus.NEW, LeadStatus.QUALIFIED, LeadStatus.DISQUALIFIED, |
| LeadStatus.CONTACTED, LeadStatus.SPAM] |
|
|
| for status in statuses: |
| lead = LeadFactory( |
| workspace_id=workspace.id, |
| status=status.value, |
| _session=db_session |
| ) |
| db_session.commit() |
| db_session.refresh(lead) |
|
|
| assert lead.status == status.value |
|
|
| |
| leads = db_session.query(Lead).filter( |
| Lead.workspace_id == workspace.id |
| ).all() |
| assert len(leads) == 5 |
|
|
| def test_lead_ai_score_range(self, db_session: Session): |
| """Test AI score is within 0.0-1.0 range.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| lead1 = LeadFactory( |
| workspace_id=workspace.id, |
| ai_score=0.0, |
| _session=db_session |
| ) |
| lead2 = LeadFactory( |
| workspace_id=workspace.id, |
| ai_score=0.5, |
| _session=db_session |
| ) |
| lead3 = LeadFactory( |
| workspace_id=workspace.id, |
| ai_score=1.0, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert 0.0 <= lead1.ai_score <= 1.0 |
| assert 0.0 <= lead2.ai_score <= 1.0 |
| assert 0.0 <= lead3.ai_score <= 1.0 |
|
|
| def test_lead_is_spam_boolean(self, db_session: Session): |
| """Test spam flag boolean.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| lead_not_spam = LeadFactory( |
| workspace_id=workspace.id, |
| is_spam=False, |
| _session=db_session |
| ) |
| lead_spam = LeadFactory( |
| workspace_id=workspace.id, |
| is_spam=True, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert lead_not_spam.is_spam is False |
| assert lead_spam.is_spam is True |
|
|
| def test_lead_is_converted_boolean(self, db_session: Session): |
| """Test conversion flag boolean.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| lead = LeadFactory( |
| workspace_id=workspace.id, |
| is_converted=True, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert lead.is_converted is True |
|
|
| def test_lead_metadata_json_field(self, db_session: Session): |
| """Test JSON metadata field.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| metadata = { |
| "source_campaign": "linkedin_q1_2026", |
| "referral_code": "REF123", |
| "lead_score": 85 |
| } |
|
|
| lead = LeadFactory( |
| workspace_id=workspace.id, |
| metadata_json=metadata, |
| _session=db_session |
| ) |
| db_session.commit() |
| db_session.refresh(lead) |
|
|
| assert lead.metadata_json["source_campaign"] == "linkedin_q1_2026" |
| assert lead.metadata_json["referral_code"] == "REF123" |
| assert lead.metadata_json["lead_score"] == 85 |
|
|
| def test_lead_external_id_indexed(self, db_session: Session): |
| """Test external ID for CRM integration.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| |
| lead = LeadFactory( |
| workspace_id=workspace.id, |
| external_id="hubspot_12345678", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| found = db_session.query(Lead).filter( |
| Lead.external_id == "hubspot_12345678" |
| ).first() |
| assert found is not None |
| assert found.id == lead.id |
|
|
|
|
| |
| |
| |
|
|
| class TestDealModel: |
| """Test Deal model (Sales deals).""" |
|
|
| def test_deal_create_with_defaults(self, db_session: Session): |
| """Test Deal creation with required fields.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
| db_session.refresh(deal) |
|
|
| assert deal.id is not None |
| assert deal.workspace_id == workspace.id |
| assert deal.name is not None |
|
|
| def test_deal_stage_enum(self, db_session: Session): |
| """Test all DealStage enum values.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| stages = [DealStage.DISCOVERY, DealStage.QUALIFICATION, DealStage.PROPOSAL, |
| DealStage.NEGOTIATION, DealStage.CLOSED_WON, DealStage.CLOSED_LOST] |
|
|
| for stage in stages: |
| deal = DealFactory( |
| workspace_id=workspace.id, |
| stage=stage.value, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| deals = db_session.query(Deal).filter( |
| Deal.workspace_id == workspace.id |
| ).all() |
| assert len(deals) == 6 |
|
|
| def test_deal_value_currency(self, db_session: Session): |
| """Test deal value and currency fields.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| value=50000.0, |
| currency="USD", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert deal.value == 50000.0 |
| assert deal.currency == "USD" |
|
|
| def test_deal_probability_range(self, db_session: Session): |
| """Test probability is within 0.0-1.0 range.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| probability=0.75, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert 0.0 <= deal.probability <= 1.0 |
|
|
| def test_deal_health_score_range(self, db_session: Session): |
| """Test health score is within 0-100 range.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| health_score=85.5, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert 0.0 <= deal.health_score <= 100.0 |
|
|
| def test_deal_negotiation_state_enum(self, db_session: Session): |
| """Test all NegotiationState enum values.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| states = [NegotiationState.INITIAL, NegotiationState.DISCOVERY, |
| NegotiationState.BARGAINING, NegotiationState.CLOSING, |
| NegotiationState.FOLLOW_UP, NegotiationState.WON, |
| NegotiationState.LOST] |
|
|
| for state in states: |
| deal = DealFactory( |
| workspace_id=workspace.id, |
| negotiation_state=state.value, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| deals = db_session.query(Deal).filter( |
| Deal.workspace_id == workspace.id |
| ).all() |
| assert len(deals) == 7 |
|
|
| def test_deal_transcripts_relationship(self, db_session: Session): |
| """Test deal has many transcripts.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| for i in range(3): |
| transcript = CallTranscriptFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| _session=db_session |
| ) |
| db_session.add(transcript) |
| db_session.commit() |
|
|
| |
| transcripts = db_session.query(CallTranscript).filter( |
| CallTranscript.deal_id == deal.id |
| ).all() |
| assert len(transcripts) == 3 |
|
|
| def test_deal_commissions_relationship(self, db_session: Session): |
| """Test deal has many commissions.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| for i in range(2): |
| commission = CommissionEntryFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| amount=5000.0, |
| _session=db_session |
| ) |
| db_session.add(commission) |
| db_session.commit() |
|
|
| |
| commissions = db_session.query(CommissionEntry).filter( |
| CommissionEntry.deal_id == deal.id |
| ).all() |
| assert len(commissions) == 2 |
|
|
| def test_deal_last_engagement_nullable(self, db_session: Session): |
| """Test nullable last_engagement_at datetime.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| last_engagement_at=None, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert deal.last_engagement_at is None |
|
|
| def test_deal_followup_count_default(self, db_session: Session): |
| """Test default followup_count is 0.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| followup_count=0, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert deal.followup_count == 0 |
|
|
|
|
| class TestDealIntelligence: |
| """Test Deal intelligence features.""" |
|
|
| def test_deal_risk_levels(self, db_session: Session): |
| """Test low/medium/high risk values.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal_low = DealFactory( |
| workspace_id=workspace.id, |
| risk_level="low", |
| _session=db_session |
| ) |
| deal_medium = DealFactory( |
| workspace_id=workspace.id, |
| risk_level="medium", |
| _session=db_session |
| ) |
| deal_high = DealFactory( |
| workspace_id=workspace.id, |
| risk_level="high", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert deal_low.risk_level == "low" |
| assert deal_medium.risk_level == "medium" |
| assert deal_high.risk_level == "high" |
|
|
| def test_deal_followup_tracking(self, db_session: Session): |
| """Test last_followup_at and followup_count update.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| last_followup_at=datetime.now(timezone.utc), |
| followup_count=5, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| deal.last_followup_at = datetime.now(timezone.utc) |
| deal.followup_count = 6 |
| db_session.commit() |
|
|
| assert deal.followup_count == 6 |
|
|
| def test_deal_metadata_json(self, db_session: Session): |
| """Test JSON for custom deal fields.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| metadata = { |
| "decision_maker": "John Doe", |
| "competitor": "CompetitorX", |
| "next_step": "Send proposal" |
| } |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| metadata_json=metadata, |
| _session=db_session |
| ) |
| db_session.commit() |
| db_session.refresh(deal) |
|
|
| assert deal.metadata_json["decision_maker"] == "John Doe" |
| assert deal.metadata_json["competitor"] == "CompetitorX" |
| assert deal.metadata_json["next_step"] == "Send proposal" |
|
|
|
|
| |
| |
| |
|
|
| class TestCommissionEntryModel: |
| """Test CommissionEntry model (Sales commissions).""" |
|
|
| def test_commission_create_with_defaults(self, db_session: Session): |
| """Test commission entry creation.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| commission = CommissionEntryFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| amount=5000.0, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert commission.id is not None |
| assert commission.deal_id == deal.id |
| assert commission.amount == 5000.0 |
|
|
| def test_commission_status_enum(self, db_session: Session): |
| """Test all CommissionStatus enum values.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| statuses = [CommissionStatus.ACCRUED, CommissionStatus.APPROVED, |
| CommissionStatus.PAID, CommissionStatus.CANCELLED] |
|
|
| for status in statuses: |
| commission = CommissionEntryFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| amount=1000.0, |
| status=status.value, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| commissions = db_session.query(CommissionEntry).filter( |
| CommissionEntry.deal_id == deal.id |
| ).all() |
| assert len(commissions) == 4 |
|
|
| def test_commission_deal_relationship(self, db_session: Session): |
| """Test commission belongs to deal.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| commission = CommissionEntryFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| amount=3000.0, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| assert commission.deal_id == deal.id |
|
|
| |
| found_deal = db_session.query(Deal).filter( |
| Deal.id == deal.id |
| ).first() |
| assert found_deal is not None |
|
|
| def test_commission_payee_nullable(self, db_session: Session): |
| """Test optional payee_id.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| commission_no_payee = CommissionEntryFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| amount=2000.0, |
| payee_id=None, |
| _session=db_session |
| ) |
| commission_with_payee = CommissionEntryFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| amount=3000.0, |
| payee_id="user_123", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert commission_no_payee.payee_id is None |
| assert commission_with_payee.payee_id == "user_123" |
|
|
| def test_commission_calculated_at_auto(self, db_session: Session): |
| """Test timestamp auto-generation.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| commission = CommissionEntryFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| amount=4000.0, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert commission.calculated_at is not None |
| assert isinstance(commission.calculated_at, datetime) |
|
|
| def test_commission_paid_at_nullable(self, db_session: Session): |
| """Test nullable paid_at timestamp.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| commission_unpaid = CommissionEntryFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| amount=1000.0, |
| paid_at=None, |
| _session=db_session |
| ) |
| commission_paid = CommissionEntryFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| amount=2000.0, |
| paid_at=datetime.now(timezone.utc), |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert commission_unpaid.paid_at is None |
| assert commission_paid.paid_at is not None |
|
|
| def test_commission_invoice_id_nullable(self, db_session: Session): |
| """Test optional invoice link.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| commission = CommissionEntryFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| amount=1500.0, |
| invoice_id="INV_001", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert commission.invoice_id == "INV_001" |
|
|
|
|
| |
| |
| |
|
|
| class TestCallTranscriptModel: |
| """Test CallTranscript model (Call transcripts).""" |
|
|
| def test_transcript_create_with_defaults(self, db_session: Session): |
| """Test transcript creation.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| transcript = CallTranscriptFactory( |
| workspace_id=workspace.id, |
| raw_transcript="This is a test transcript.", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert transcript.id is not None |
| assert transcript.raw_transcript == "This is a test transcript." |
|
|
| def test_transcript_raw_text_field(self, db_session: Session): |
| """Test large text field.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| large_text = "Word " * 1000 |
|
|
| transcript = CallTranscriptFactory( |
| workspace_id=workspace.id, |
| raw_transcript=large_text, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert len(transcript.raw_transcript) == len(large_text) |
|
|
| def test_transcript_deal_relationship(self, db_session: Session): |
| """Test optional deal link.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| transcript = CallTranscriptFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| raw_transcript="Deal discussion transcript", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert transcript.deal_id == deal.id |
|
|
| |
| transcript_no_deal = CallTranscriptFactory( |
| workspace_id=workspace.id, |
| deal_id=None, |
| raw_transcript="General transcript", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert transcript_no_deal.deal_id is None |
|
|
| def test_transcript_meeting_id_nullable(self, db_session: Session): |
| """Test optional meeting ID.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| transcript = CallTranscriptFactory( |
| workspace_id=workspace.id, |
| raw_transcript="Test transcript", |
| meeting_id="zoom_12345", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert transcript.meeting_id == "zoom_12345" |
|
|
| def test_transcript_objections_json(self, db_session: Session): |
| """Test JSON array of objections.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| objections = [ |
| {"objection": "Price too high", "severity": "high"}, |
| {"objection": "Need to check with team", "severity": "low"} |
| ] |
|
|
| transcript = CallTranscriptFactory( |
| workspace_id=workspace.id, |
| raw_transcript="Customer raised objections", |
| objections=objections, |
| _session=db_session |
| ) |
| db_session.commit() |
| db_session.refresh(transcript) |
|
|
| assert len(transcript.objections) == 2 |
| assert transcript.objections[0]["objection"] == "Price too high" |
|
|
| def test_transcript_action_items_json(self, db_session: Session): |
| """Test JSON array of tasks.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| action_items = [ |
| {"task": "Send case study", "priority": "high", "due": "2026-03-15"}, |
| {"task": "Schedule demo", "priority": "medium", "due": "2026-03-16"} |
| ] |
|
|
| transcript = CallTranscriptFactory( |
| workspace_id=workspace.id, |
| raw_transcript="Action items discussed", |
| action_items=action_items, |
| _session=db_session |
| ) |
| db_session.commit() |
| db_session.refresh(transcript) |
|
|
| assert len(transcript.action_items) == 2 |
| assert transcript.action_items[0]["task"] == "Send case study" |
|
|
| def test_transcript_summary_optional(self, db_session: Session): |
| """Test optional AI summary.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| transcript_with_summary = CallTranscriptFactory( |
| workspace_id=workspace.id, |
| raw_transcript="Full transcript here", |
| summary="Customer is interested in our premium plan.", |
| _session=db_session |
| ) |
| transcript_no_summary = CallTranscriptFactory( |
| workspace_id=workspace.id, |
| raw_transcript="Another transcript", |
| summary=None, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert transcript_with_summary.summary is not None |
| assert transcript_no_summary.summary is None |
|
|
| def test_transcript_metadata_json(self, db_session: Session): |
| """Test additional metadata.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| metadata = { |
| "recording_url": "s3://recordings/call_123.mp4", |
| "duration_seconds": 1800, |
| "participants": ["Agent", "Customer"] |
| } |
|
|
| transcript = CallTranscriptFactory( |
| workspace_id=workspace.id, |
| raw_transcript="Test", |
| metadata_json=metadata, |
| _session=db_session |
| ) |
| db_session.commit() |
| db_session.refresh(transcript) |
|
|
| assert transcript.metadata_json["duration_seconds"] == 1800 |
| assert len(transcript.metadata_json["participants"]) == 2 |
|
|
|
|
| |
| |
| |
|
|
| class TestFollowUpTaskModel: |
| """Test FollowUpTask model (Sales tasks).""" |
|
|
| def test_followup_create_with_defaults(self, db_session: Session): |
| """Test follow-up task creation.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| task = FollowUpTaskFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| description="Send follow-up email", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert task.id is not None |
| assert task.deal_id == deal.id |
| assert task.description == "Send follow-up email" |
|
|
| def test_followup_deal_relationship(self, db_session: Session): |
| """Test task belongs to deal.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| task = FollowUpTaskFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| description="Call customer", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert task.deal_id == deal.id |
|
|
| def test_followup_description_required(self, db_session: Session): |
| """Test description is required.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| task = FollowUpTaskFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| description="Required description field", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert task.description is not None |
| assert len(task.description) > 0 |
|
|
| def test_followup_suggested_date_nullable(self, db_session: Session): |
| """Test optional date.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| task_with_date = FollowUpTaskFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| description="Task with date", |
| suggested_date=datetime.now(timezone.utc) + timedelta(days=3), |
| _session=db_session |
| ) |
| task_no_date = FollowUpTaskFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| description="Task without date", |
| suggested_date=None, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert task_with_date.suggested_date is not None |
| assert task_no_date.suggested_date is None |
|
|
| def test_followup_is_completed_boolean(self, db_session: Session): |
| """Test completion flag.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| task_pending = FollowUpTaskFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| description="Pending task", |
| is_completed=False, |
| _session=db_session |
| ) |
| task_done = FollowUpTaskFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| description="Completed task", |
| is_completed=True, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert task_pending.is_completed is False |
| assert task_done.is_completed is True |
|
|
| def test_followup_ai_rationale_optional(self, db_session: Session): |
| """Test AI explanation.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| task = FollowUpTaskFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| description="AI suggested task", |
| ai_rationale="Customer showed interest during call, follow up recommended.", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert task.ai_rationale is not None |
| assert "interest" in task.ai_rationale.lower() |
|
|
|
|
| |
| |
| |
|
|
| class TestContractModel: |
| """Test Contract model (Service contracts).""" |
|
|
| def test_contract_create_with_defaults(self, db_session: Session): |
| """Test contract creation.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| contract = ContractFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert contract.id is not None |
| assert contract.workspace_id == workspace.id |
| assert contract.name is not None |
|
|
| def test_contract_type_enum(self, db_session: Session): |
| """Test all ContractType enum values.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| types = [ContractType.FIXED_FEE, ContractType.RETAINER, ContractType.TIME_MATERIAL] |
|
|
| for contract_type in types: |
| contract = ContractFactory( |
| workspace_id=workspace.id, |
| type=contract_type.value, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| contracts = db_session.query(Contract).filter( |
| Contract.workspace_id == workspace.id |
| ).all() |
| assert len(contracts) == 3 |
|
|
| def test_contract_deal_relationship(self, db_session: Session): |
| """Test optional deal link from sales.Deal.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| deal = DealFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| contract = ContractFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert contract.deal_id == deal.id |
|
|
| |
| contract_no_deal = ContractFactory( |
| workspace_id=workspace.id, |
| deal_id=None, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert contract_no_deal.deal_id is None |
|
|
| def test_contract_total_amount_float(self, db_session: Session): |
| """Test currency amount.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| contract = ContractFactory( |
| workspace_id=workspace.id, |
| total_amount=100000.0, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert contract.total_amount == 100000.0 |
|
|
| def test_contract_date_range(self, db_session: Session): |
| """Test start_date and end_date.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| start = datetime.now(timezone.utc) - timedelta(days=30) |
| end = datetime.now(timezone.utc) + timedelta(days=365) |
|
|
| contract = ContractFactory( |
| workspace_id=workspace.id, |
| start_date=start, |
| end_date=end, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert contract.start_date is not None |
| assert contract.end_date is not None |
| assert contract.end_date > contract.start_date |
|
|
| def test_contract_metadata_json(self, db_session: Session): |
| """Test JSON for contract terms.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| metadata = { |
| "payment_terms": "net_30", |
| "renewal_auto": True, |
| "notice_period_days": 30 |
| } |
|
|
| contract = ContractFactory( |
| workspace_id=workspace.id, |
| metadata_json=metadata, |
| _session=db_session |
| ) |
| db_session.commit() |
| db_session.refresh(contract) |
|
|
| assert contract.metadata_json["payment_terms"] == "net_30" |
| assert contract.metadata_json["renewal_auto"] is True |
|
|
| def test_contract_projects_relationship(self, db_session: Session): |
| """Test contract has many projects.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| contract = ContractFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| for i in range(3): |
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| contract_id=contract.id, |
| _session=db_session |
| ) |
| db_session.add(project) |
| db_session.commit() |
|
|
| |
| projects = db_session.query(Project).filter( |
| Project.contract_id == contract.id |
| ).all() |
| assert len(projects) == 3 |
|
|
|
|
| |
| |
| |
|
|
| class TestProjectModel: |
| """Test Project model (Service projects).""" |
|
|
| def test_project_create_with_defaults(self, db_session: Session): |
| """Test project creation.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert project.id is not None |
| assert project.workspace_id == workspace.id |
| assert project.name is not None |
|
|
| def test_project_status_enum(self, db_session: Session): |
| """Test all ProjectStatus enum values.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| statuses = [ProjectStatus.PENDING, ProjectStatus.ACTIVE, ProjectStatus.PAUSED_PAYMENT, |
| ProjectStatus.PAUSED_CLIENT, ProjectStatus.COMPLETED, ProjectStatus.CANCELED] |
|
|
| for status in statuses: |
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| status=status.value, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| projects = db_session.query(Project).filter( |
| Project.workspace_id == workspace.id |
| ).all() |
| assert len(projects) == 6 |
|
|
| def test_project_contract_relationship(self, db_session: Session): |
| """Test optional contract link.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| contract = ContractFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| contract_id=contract.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert project.contract_id == contract.id |
|
|
| |
| project_no_contract = ProjectFactory( |
| workspace_id=workspace.id, |
| contract_id=None, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert project_no_contract.contract_id is None |
|
|
| def test_project_budget_tracking(self, db_session: Session): |
| """Test budget_hours/amount vs actual.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| budget_hours=1000.0, |
| actual_hours=650.0, |
| budget_amount=100000.0, |
| actual_burn=65000.0, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert project.budget_hours == 1000.0 |
| assert project.actual_hours == 650.0 |
| assert project.budget_amount == 100000.0 |
| assert project.actual_burn == 65000.0 |
|
|
| def test_project_budget_status_enum(self, db_session: Session): |
| """Test BudgetStatus values (on_track, at_risk, over_budget).""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| statuses = [BudgetStatus.ON_TRACK, BudgetStatus.AT_RISK, BudgetStatus.OVER_BUDGET] |
|
|
| for status in statuses: |
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| budget_status=status.value, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| projects = db_session.query(Project).filter( |
| Project.workspace_id == workspace.id |
| ).all() |
| assert len(projects) == 3 |
|
|
| def test_project_guardrail_thresholds(self, db_session: Session): |
| """Test warn, pause, block percentages.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| warn_threshold_pct=80, |
| pause_threshold_pct=90, |
| block_threshold_pct=100, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert project.warn_threshold_pct == 80 |
| assert project.pause_threshold_pct == 90 |
| assert project.block_threshold_pct == 100 |
|
|
| def test_project_guardrail_validation(self, db_session: Session): |
| """Verify warn < pause < block (application-level validation).""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| |
| project_valid = ProjectFactory( |
| workspace_id=workspace.id, |
| warn_threshold_pct=70, |
| pause_threshold_pct=85, |
| block_threshold_pct=100, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| |
| assert project_valid.warn_threshold_pct < project_valid.pause_threshold_pct |
| assert project_valid.pause_threshold_pct < project_valid.block_threshold_pct |
|
|
| def test_project_priority_levels(self, db_session: Session): |
| """Test low/medium/high/critical.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| priorities = ["low", "medium", "high", "critical"] |
|
|
| for priority in priorities: |
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| priority=priority, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| projects = db_session.query(Project).filter( |
| Project.workspace_id == workspace.id |
| ).all() |
| assert len(projects) == 4 |
|
|
| def test_project_milestones_relationship(self, db_session: Session): |
| """Test project has many milestones.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| for i in range(5): |
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| order=i, |
| _session=db_session |
| ) |
| db_session.add(milestone) |
| db_session.commit() |
|
|
| |
| milestones = db_session.query(Milestone).filter( |
| Milestone.project_id == project.id |
| ).all() |
| assert len(milestones) == 5 |
|
|
| def test_project_dates(self, db_session: Session): |
| """Test planned vs actual dates.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| planned_start = datetime.now(timezone.utc) - timedelta(days=15) |
| planned_end = datetime.now(timezone.utc) + timedelta(days=90) |
| actual_start = datetime.now(timezone.utc) - timedelta(days=10) |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| planned_start_date=planned_start, |
| planned_end_date=planned_end, |
| actual_start_date=actual_start, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert project.planned_start_date is not None |
| assert project.planned_end_date is not None |
| assert project.actual_start_date is not None |
|
|
| def test_project_risk_fields(self, db_session: Session): |
| """Test risk_score and risk_level.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| risk_level="medium", |
| risk_score=55.5, |
| risk_rationale="Timeline at risk due to scope changes", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert project.risk_level == "medium" |
| assert project.risk_score == 55.5 |
| assert "scope changes" in project.risk_rationale.lower() |
|
|
|
|
| |
| |
| |
|
|
| class TestMilestoneModel: |
| """Test Milestone model (Project milestones).""" |
|
|
| def test_milestone_create_with_defaults(self, db_session: Session): |
| """Test milestone creation.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert milestone.id is not None |
| assert milestone.project_id == project.id |
| assert milestone.name is not None |
|
|
| def test_milestone_status_enum(self, db_session: Session): |
| """Test all MilestoneStatus enum values.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| statuses = [MilestoneStatus.PENDING, MilestoneStatus.IN_PROGRESS, |
| MilestoneStatus.COMPLETED, MilestoneStatus.APPROVED, MilestoneStatus.INVOICED] |
|
|
| for status in statuses: |
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| status=status.value, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| milestones = db_session.query(Milestone).filter( |
| Milestone.project_id == project.id |
| ).all() |
| assert len(milestones) == 5 |
|
|
| def test_milestone_project_relationship(self, db_session: Session): |
| """Test milestone belongs to project.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert milestone.project_id == project.id |
|
|
| def test_milestone_billing_amount(self, db_session: Session): |
| """Test amount for invoicing.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| amount=25000.0, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert milestone.amount == 25000.0 |
|
|
| def test_milestone_percentage(self, db_session: Session): |
| """Test % of contract.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| percentage=25.0, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert milestone.percentage == 25.0 |
|
|
| def test_milestone_ordering(self, db_session: Session): |
| """Test order field for sequencing.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| milestone3 = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| order=3, |
| _session=db_session |
| ) |
| milestone1 = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| order=1, |
| _session=db_session |
| ) |
| milestone2 = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| order=2, |
| _session=db_session |
| ) |
| db_session.add_all([milestone3, milestone1, milestone2]) |
| db_session.commit() |
|
|
| |
| milestones = db_session.query(Milestone).filter( |
| Milestone.project_id == project.id |
| ).order_by(Milestone.order).all() |
|
|
| assert milestones[0].order == 1 |
| assert milestones[1].order == 2 |
| assert milestones[2].order == 3 |
|
|
| def test_milestone_budget_tracking(self, db_session: Session): |
| """Test actual_burn and budget_status.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| actual_burn=22000.0, |
| budget_status=BudgetStatus.ON_TRACK.value, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert milestone.actual_burn == 22000.0 |
| assert milestone.budget_status == BudgetStatus.ON_TRACK.value |
|
|
| def test_milestone_invoice_id_nullable(self, db_session: Session): |
| """Test optional invoice link.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| milestone_no_invoice = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| invoice_id=None, |
| _session=db_session |
| ) |
| milestone_with_invoice = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| invoice_id="INV_MILESTONE_001", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert milestone_no_invoice.invoice_id is None |
| assert milestone_with_invoice.invoice_id == "INV_MILESTONE_001" |
|
|
| def test_milestone_tasks_relationship(self, db_session: Session): |
| """Test milestone has many tasks.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| for i in range(4): |
| task = ProjectTaskFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| milestone_id=milestone.id, |
| _session=db_session |
| ) |
| db_session.add(task) |
| db_session.commit() |
|
|
| |
| tasks = db_session.query(ProjectTask).filter( |
| ProjectTask.milestone_id == milestone.id |
| ).all() |
| assert len(tasks) == 4 |
|
|
|
|
| |
| |
| |
|
|
| class TestProjectTaskModel: |
| """Test ProjectTask model (Project tasks).""" |
|
|
| def test_task_create_with_defaults(self, db_session: Session): |
| """Test project task creation.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| task = ProjectTaskFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| milestone_id=milestone.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert task.id is not None |
| assert task.project_id == project.id |
| assert task.milestone_id == milestone.id |
|
|
| def test_task_milestone_relationship(self, db_session: Session): |
| """Test task belongs to milestone.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| task = ProjectTaskFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| milestone_id=milestone.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert task.milestone_id == milestone.id |
|
|
| def test_task_project_relationship(self, db_session: Session): |
| """Test task belongs to project.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| task = ProjectTaskFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| milestone_id=milestone.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert task.project_id == project.id |
|
|
| def test_task_status_values(self, db_session: Session): |
| """Test pending, in_progress, completed, blocked.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| statuses = ["pending", "in_progress", "completed", "blocked"] |
|
|
| for status in statuses: |
| task = ProjectTaskFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| milestone_id=milestone.id, |
| status=status, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| tasks = db_session.query(ProjectTask).filter( |
| ProjectTask.project_id == project.id |
| ).all() |
| assert len(tasks) == 4 |
|
|
| def test_task_assigned_to_relationship(self, db_session: Session): |
| """Test optional user assignment.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| task_assigned = ProjectTaskFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| milestone_id=milestone.id, |
| assigned_to="user_123", |
| _session=db_session |
| ) |
| task_unassigned = ProjectTaskFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| milestone_id=milestone.id, |
| assigned_to=None, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert task_assigned.assigned_to == "user_123" |
| assert task_unassigned.assigned_to is None |
|
|
| def test_task_due_date_nullable(self, db_session: Session): |
| """Test optional deadline.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| task_with_due = ProjectTaskFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| milestone_id=milestone.id, |
| due_date=datetime.now(timezone.utc) + timedelta(days=7), |
| _session=db_session |
| ) |
| task_no_due = ProjectTaskFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| milestone_id=milestone.id, |
| due_date=None, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert task_with_due.due_date is not None |
| assert task_no_due.due_date is None |
|
|
| def test_task_completed_at_nullable(self, db_session: Session): |
| """Test nullable completion timestamp.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| task_completed = ProjectTaskFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| milestone_id=milestone.id, |
| status="completed", |
| completed_at=datetime.now(timezone.utc), |
| _session=db_session |
| ) |
| task_pending = ProjectTaskFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| milestone_id=milestone.id, |
| status="pending", |
| completed_at=None, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert task_completed.completed_at is not None |
| assert task_pending.completed_at is None |
|
|
| def test_task_actual_hours_tracking(self, db_session: Session): |
| """Test time tracking.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| task = ProjectTaskFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| milestone_id=milestone.id, |
| actual_hours=8.5, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert task.actual_hours == 8.5 |
|
|
| def test_task_metadata_json(self, db_session: Session): |
| """Test JSON for task data.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| metadata = { |
| "dependencies": ["task_1", "task_2"], |
| "tags": ["frontend", "urgent"], |
| "estimated_hours": 12 |
| } |
|
|
| task = ProjectTaskFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| milestone_id=milestone.id, |
| metadata_json=metadata, |
| _session=db_session |
| ) |
| db_session.commit() |
| db_session.refresh(task) |
|
|
| assert len(task.metadata_json["dependencies"]) == 2 |
| assert "frontend" in task.metadata_json["tags"] |
|
|
|
|
| |
| |
| |
|
|
| class TestAppointmentModel: |
| """Test Appointment model (Service appointments).""" |
|
|
| def test_appointment_create_with_defaults(self, db_session: Session): |
| """Test appointment creation.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| |
| customer = Entity( |
| id=str(__import__('uuid').uuid4()), |
| workspace_id=workspace.id, |
| name="Test Customer", |
| type="customer", |
| email="customer@test.com" |
| ) |
| db_session.add(customer) |
| db_session.commit() |
|
|
| appointment = AppointmentFactory( |
| workspace_id=workspace.id, |
| customer_id=customer.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert appointment.id is not None |
| assert appointment.workspace_id == workspace.id |
| assert appointment.customer_id == customer.id |
|
|
| def test_appointment_customer_relationship(self, db_session: Session): |
| """Test appointment belongs to Entity (from accounting).""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| |
| customer = Entity( |
| id=str(__import__('uuid').uuid4()), |
| workspace_id=workspace.id, |
| name="John Doe", |
| type="customer", |
| email="john@test.com" |
| ) |
| db_session.add(customer) |
| db_session.commit() |
|
|
| appointment = AppointmentFactory( |
| workspace_id=workspace.id, |
| customer_id=customer.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert appointment.customer_id == customer.id |
|
|
| |
| found_customer = db_session.query(Entity).filter( |
| Entity.id == customer.id |
| ).first() |
| assert found_customer is not None |
| assert found_customer.name == "John Doe" |
|
|
| def test_appointment_service_relationship(self, db_session: Session): |
| """Test optional service link.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| |
| customer = Entity( |
| id=str(__import__('uuid').uuid4()), |
| workspace_id=workspace.id, |
| name="Test Customer", |
| type="customer", |
| email="test@example.com" |
| ) |
| db_session.add(customer) |
| db_session.commit() |
|
|
| appointment_with_service = AppointmentFactory( |
| workspace_id=workspace.id, |
| customer_id=customer.id, |
| service_id="service_123", |
| _session=db_session |
| ) |
| appointment_no_service = AppointmentFactory( |
| workspace_id=workspace.id, |
| customer_id=customer.id, |
| service_id=None, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert appointment_with_service.service_id == "service_123" |
| assert appointment_no_service.service_id is None |
|
|
| def test_appointment_status_enum(self, db_session: Session): |
| """Test all AppointmentStatus enum values.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| |
| customer = Entity( |
| id=str(__import__('uuid').uuid4()), |
| workspace_id=workspace.id, |
| name="Test Customer", |
| type="customer", |
| email="customer@test.com" |
| ) |
| db_session.add(customer) |
| db_session.commit() |
|
|
| statuses = [AppointmentStatus.SCHEDULED, AppointmentStatus.COMPLETED, |
| AppointmentStatus.NO_SHOW, AppointmentStatus.CANCELED] |
|
|
| for status in statuses: |
| appointment = AppointmentFactory( |
| workspace_id=workspace.id, |
| customer_id=customer.id, |
| status=status.value, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| appointments = db_session.query(Appointment).filter( |
| Appointment.customer_id == customer.id |
| ).all() |
| assert len(appointments) == 4 |
|
|
| def test_appointment_time_range(self, db_session: Session): |
| """Test start_time < end_time.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| |
| customer = Entity( |
| id=str(__import__('uuid').uuid4()), |
| workspace_id=workspace.id, |
| name="Test Customer", |
| type="customer", |
| email="customer@test.com" |
| ) |
| db_session.add(customer) |
| db_session.commit() |
|
|
| base_time = datetime.now(timezone.utc).replace(microsecond=0) |
| start = base_time + timedelta(days=1, hours=10) |
| end = base_time + timedelta(days=1, hours=11) |
|
|
| appointment = AppointmentFactory( |
| workspace_id=workspace.id, |
| customer_id=customer.id, |
| start_time=start, |
| end_time=end, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert appointment.start_time < appointment.end_time |
| assert (appointment.end_time - appointment.start_time).total_seconds() == 3600 |
|
|
| def test_appointment_deposit_fields(self, db_session: Session): |
| """Test deposit_amount and is_deposit_paid.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| |
| customer = Entity( |
| id=str(__import__('uuid').uuid4()), |
| workspace_id=workspace.id, |
| name="Test Customer", |
| type="customer", |
| email="customer@test.com" |
| ) |
| db_session.add(customer) |
| db_session.commit() |
|
|
| appointment = AppointmentFactory( |
| workspace_id=workspace.id, |
| customer_id=customer.id, |
| deposit_amount=100.0, |
| is_deposit_paid=True, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| assert appointment.deposit_amount == 100.0 |
| assert appointment.is_deposit_paid is True |
|
|
| def test_appointment_metadata_json(self, db_session: Session): |
| """Test JSON for travel heuristics.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| |
| customer = Entity( |
| id=str(__import__('uuid').uuid4()), |
| workspace_id=workspace.id, |
| name="Test Customer", |
| type="customer", |
| email="customer@test.com" |
| ) |
| db_session.add(customer) |
| db_session.commit() |
|
|
| metadata = { |
| "travel_distance_km": 25.5, |
| "travel_time_minutes": 45, |
| "requires_equipment": True |
| } |
|
|
| appointment = AppointmentFactory( |
| workspace_id=workspace.id, |
| customer_id=customer.id, |
| metadata_json=metadata, |
| _session=db_session |
| ) |
| db_session.commit() |
| db_session.refresh(appointment) |
|
|
| assert appointment.metadata_json["travel_distance_km"] == 25.5 |
| assert appointment.metadata_json["requires_equipment"] is True |
|
|
|
|
| |
| |
| |
|
|
| class TestServiceDeliveryWorkflows: |
| """Test cross-module workflows and relationships.""" |
|
|
| def test_contract_to_project_creation(self, db_session: Session): |
| """Test create contract, then linked project.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| |
| contract = ContractFactory( |
| workspace_id=workspace.id, |
| name="Service Contract 001", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| contract_id=contract.id, |
| name="Project 001", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| assert project.contract_id == contract.id |
|
|
| |
| found_project = db_session.query(Project).filter( |
| Project.contract_id == contract.id |
| ).first() |
| assert found_project is not None |
| assert found_project.name == "Project 001" |
|
|
| def test_project_to_milestone_chain(self, db_session: Session): |
| """Test create project with multiple milestones.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| |
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| name="Milestone Project", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| for i in range(5): |
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| name=f"Milestone {i+1}", |
| order=i, |
| _session=db_session |
| ) |
| db_session.add(milestone) |
| db_session.commit() |
|
|
| |
| milestones = db_session.query(Milestone).filter( |
| Milestone.project_id == project.id |
| ).order_by(Milestone.order).all() |
|
|
| assert len(milestones) == 5 |
| assert milestones[0].name == "Milestone 1" |
| assert milestones[4].name == "Milestone 5" |
|
|
| def test_milestone_to_task_hierarchy(self, db_session: Session): |
| """Test create milestone with multiple tasks.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| |
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| name="Task Milestone", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| for i in range(6): |
| task = ProjectTaskFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| milestone_id=milestone.id, |
| name=f"Task {i+1}", |
| _session=db_session |
| ) |
| db_session.add(task) |
| db_session.commit() |
|
|
| |
| tasks = db_session.query(ProjectTask).filter( |
| ProjectTask.milestone_id == milestone.id |
| ).all() |
|
|
| assert len(tasks) == 6 |
|
|
| def test_deal_to_contract_conversion(self, db_session: Session): |
| """Test create deal, then convert to contract.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| |
| deal = DealFactory( |
| workspace_id=workspace.id, |
| name="Enterprise Deal", |
| stage=DealStage.CLOSED_WON.value, |
| value=100000.0, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| contract = ContractFactory( |
| workspace_id=workspace.id, |
| deal_id=deal.id, |
| name="Enterprise Contract", |
| total_amount=deal.value, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| assert contract.deal_id == deal.id |
| assert contract.total_amount == deal.value |
|
|
| |
| found_contract = db_session.query(Contract).filter( |
| Contract.deal_id == deal.id |
| ).first() |
| assert found_contract is not None |
|
|
| def test_project_budget_status_calculation(self, db_session: Session): |
| """Test on_track/at_risk/over_budget logic.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| |
| project_on_track = ProjectFactory( |
| workspace_id=workspace.id, |
| budget_amount=100000.0, |
| actual_burn=60000.0, |
| budget_status=BudgetStatus.ON_TRACK.value, |
| _session=db_session |
| ) |
|
|
| |
| project_at_risk = ProjectFactory( |
| workspace_id=workspace.id, |
| budget_amount=100000.0, |
| actual_burn=85000.0, |
| budget_status=BudgetStatus.AT_RISK.value, |
| _session=db_session |
| ) |
|
|
| |
| project_over_budget = ProjectFactory( |
| workspace_id=workspace.id, |
| budget_amount=100000.0, |
| actual_burn=110000.0, |
| budget_status=BudgetStatus.OVER_BUDGET.value, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| assert project_on_track.budget_status == BudgetStatus.ON_TRACK.value |
| assert project_at_risk.budget_status == BudgetStatus.AT_RISK.value |
| assert project_over_budget.budget_status == BudgetStatus.OVER_BUDGET.value |
|
|
|
|
| |
| |
| |
|
|
| class TestConcurrentAccess: |
| """Test concurrent session access patterns with proper isolation.""" |
|
|
| def test_separate_sessions_isolate_changes(self, db_session: Session): |
| """Test that separate db_session instances don't interfere.""" |
| |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| |
| deal = DealFactory( |
| workspace_id=workspace.id, |
| stage=DealStage.DISCOVERY.value, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| found = db_session.query(Deal).filter(Deal.id == deal.id).first() |
| assert found is not None |
| assert found.stage == DealStage.DISCOVERY.value |
|
|
| |
| deal.stage = DealStage.NEGOTIATION.value |
| db_session.commit() |
|
|
| |
| found = db_session.query(Deal).filter(Deal.id == deal.id).first() |
| assert found.stage == DealStage.NEGOTIATION.value |
|
|
| def test_session_rollback_after_test(self, db_session: Session): |
| """Test that db_session fixture rolls back after test.""" |
| |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| |
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| status=ProjectStatus.ACTIVE.value, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| count = db_session.query(Project).filter( |
| Project.workspace_id == workspace.id |
| ).count() |
| assert count == 1 |
|
|
| |
| |
|
|
| def test_multiple_operations_in_single_session(self, db_session: Session): |
| """Test multiple related operations in a single session.""" |
| workspace = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| |
| contract = ContractFactory( |
| workspace_id=workspace.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| project = ProjectFactory( |
| workspace_id=workspace.id, |
| contract_id=contract.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| milestone = MilestoneFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| task = ProjectTaskFactory( |
| workspace_id=workspace.id, |
| project_id=project.id, |
| milestone_id=milestone.id, |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| assert contract.id == project.contract_id |
| assert project.id == milestone.project_id |
| assert milestone.id == task.milestone_id |
|
|
| |
| found_project = db_session.query(Project).filter( |
| Project.contract_id == contract.id |
| ).first() |
| assert found_project is not None |
| assert found_project.id == project.id |
|
|
| found_milestone = db_session.query(Milestone).filter( |
| Milestone.project_id == project.id |
| ).first() |
| assert found_milestone is not None |
| assert found_milestone.id == milestone.id |
|
|
| def test_session_isolation_with_factory_injection(self, db_session: Session): |
| """Test that factory session injection creates isolated data.""" |
| workspace1 = WorkspaceFactory(_session=db_session) |
| workspace2 = WorkspaceFactory(_session=db_session) |
| db_session.commit() |
|
|
| |
| contract1 = ContractFactory( |
| workspace_id=workspace1.id, |
| name="Contract 1", |
| _session=db_session |
| ) |
| contract2 = ContractFactory( |
| workspace_id=workspace2.id, |
| name="Contract 2", |
| _session=db_session |
| ) |
| db_session.commit() |
|
|
| |
| workspace1_contracts = db_session.query(Contract).filter( |
| Contract.workspace_id == workspace1.id |
| ).all() |
| assert len(workspace1_contracts) == 1 |
| assert workspace1_contracts[0].name == "Contract 1" |
|
|
| workspace2_contracts = db_session.query(Contract).filter( |
| Contract.workspace_id == workspace2.id |
| ).all() |
| assert len(workspace2_contracts) == 1 |
| assert workspace2_contracts[0].name == "Contract 2" |
|
|