from contextlib import contextmanager from app.workflows import preview class ReadOnlyCursor: def __init__(self, agreements=(), cards=(), rules=()): self.pools = {'customer_fixed_price_agreements': agreements, 'tticket_prepaid_cards': cards, 'bmc_workflows': rules} self.queries = [] self.rows = [] def execute(self, sql, params=None): assert sql.startswith('SELECT '), 'Preview must not write' self.queries.append((sql, params)) self.rows = next((rows for table, rows in self.pools.items() if table in sql), []) def fetchall(self): return self.rows def setup_preview(monkeypatch, cursor, active=True): @contextmanager def transaction(): yield cursor monkeypatch.setattr(preview, 'transaction', transaction) monkeypatch.setattr(preview, 'load_case_context', lambda *args: {'id': 10, 'customer_id': '7', 'tag_ids': [3], 'concrete_tag_ids': [3], 'titel': 'Test', 'beskrivelse': 'Case text'}) monkeypatch.setattr(preview, 'active_for_customer', lambda customer_id: active) monkeypatch.setattr(preview, 'enrich_entity_context', lambda cursor, context, entity_type, entity_id: context) def test_inactive_returns_draft_without_loading_rules_or_resources(monkeypatch): cursor = ReadOnlyCursor() setup_preview(monkeypatch, cursor, active=False) assert preview.preview_case_billing(10)['method'] == 'draft' assert cursor.queries == [] def test_coverage_predicts_fast_price_without_saving_or_exposing_trace(monkeypatch): cursor = ReadOnlyCursor(agreements=[{'id': 9, 'workflow_coverage': {'tag_ids': [3]}}]) setup_preview(monkeypatch, cursor) assert preview.preview_case_billing(10) == {'status': 'decided', 'method': 'subscription', 'reason': 'resource_coverage', 'agreement_id': 9} assert all(params is None or params[0] == 7 for sql, params in cursor.queries) assert 'expires_at>now()' in cursor.queries[1][0] assert 'cancellation_effective_date' in cursor.queries[0][0] def test_explicit_rule_takes_precedence_over_coverage(monkeypatch): rule = {'id': 1, 'definition': {'trigger': 'time.created', 'branches': [{'condition': {'field': 'description', 'operator': 'contains', 'value': 'intern'}, 'actions': [{'type': 'set_billing', 'method': 'internal'}]}]}} cursor = ReadOnlyCursor(agreements=[{'id': 9, 'workflow_coverage': {'tag_ids': [3]}}], rules=[rule]) setup_preview(monkeypatch, cursor) assert preview.preview_case_billing(10, description='intern opgave')['method'] == 'internal' def test_prepaid_prediction_checks_requested_minutes(monkeypatch): cursor = ReadOnlyCursor(cards=[{'id': 4, 'remaining_hours': .5, 'workflow_coverage': {'tag_ids': [3]}}]) setup_preview(monkeypatch, cursor) assert preview.preview_case_billing(10, minutes=30)['method'] == 'prepaid' assert preview.preview_case_billing(10, minutes=60)['reason'] == 'insufficient_card_balance' def test_pending_tag_review_remains_blocking(monkeypatch): cursor = ReadOnlyCursor(agreements=[{'id': 9, 'workflow_coverage': {'tag_ids': [3]}}]) setup_preview(monkeypatch, cursor) monkeypatch.setattr(preview, 'enrich_entity_context', lambda cursor, context, entity_type, entity_id: dict(context, pending_billing_tag_suggestions=['case_proposal:1'])) assert preview.preview_case_billing(10)['reason'] == 'pending_tag_approval'