from __future__ import annotations from dataclasses import dataclass from pathlib import Path from typing import Any import yaml @dataclass(frozen=True) class FieldMapping: header: str field: str kind: str admission_account_id: bool = False @dataclass(frozen=True) class RevenueSyncConfig: path: Path data: dict[str, Any] @property def name(self) -> str: return str(self.data["job"]["name"]) @property def source(self) -> dict[str, Any]: return dict(self.data["source"]) @property def target(self) -> dict[str, Any]: return dict(self.data["target"]) @property def candidate(self) -> dict[str, Any]: return dict(self.data["candidate"]) @property def execution(self) -> dict[str, Any]: return dict(self.data["execution"]) @property def mappings(self) -> tuple[FieldMapping, ...]: return tuple( FieldMapping( header=str(item["header"]), field=str(item["field"]), kind=str(item["kind"]), admission_account_id=bool(item.get("admission_account_id", False)), ) for item in self.data["mapping"] ) @property def account_id_mapping(self) -> FieldMapping: return next(mapping for mapping in self.mappings if mapping.admission_account_id) def load_revenue_sync_config(path: str | Path) -> RevenueSyncConfig: config_path = Path(path).resolve() config = RevenueSyncConfig(path=config_path, data=yaml.safe_load(config_path.read_text())) _validate(config) return config def _validate(config: RevenueSyncConfig) -> None: if config.data.get("version") != 1: raise ValueError("netsuite revenue sync config version must be 1") mappings = config.mappings if len(mappings) != 35: raise ValueError("mapping must define exactly 35 business fields") if len({item.header for item in mappings}) != len(mappings): raise ValueError("mapping headers must be unique") if len({item.field for item in mappings}) != len(mappings): raise ValueError("mapping target fields must be unique") if sum(item.admission_account_id for item in mappings) != 1: raise ValueError("mapping must mark exactly one admission_account_id field") if any(item.kind not in {"text", "number", "currency", "date", "datetime"} for item in mappings): raise ValueError("mapping kinds must be text, number, currency, date, or datetime") if int(config.source["page_size"]) != 500: raise ValueError("source.page_size must remain 500") if config.source["account_realm"] != "7429932": raise ValueError("source.account_realm must be the bound NetSuite realm") if config.source["saved_search_id"] != "customsearch1710": raise ValueError("source.saved_search_id must remain customsearch1710") if config.execution.get("soft_deactivation_enabled") is not False: raise ValueError("v1 must keep soft deactivation disabled") if config.target["object"] != "NetSuite_Revenue_Voucher_Line__c": raise ValueError("target.object must be NetSuite_Revenue_Voucher_Line__c") if int(config.execution.get("crm_upsert_batch_size", 0)) not in range(1, 201): raise ValueError("execution.crm_upsert_batch_size must be 1..200") expected_controls = { "account_realm", "transaction_id", "line_unique_key", "accounting_book_id", "posting_period_id", "search_id", "schema_version", "payload_hash", "first_seen_run_id", "first_seen_at", "last_seen_run_id", "last_seen_at", "is_source_active", } if set(config.target["controls"]) != expected_controls: raise ValueError("target.controls must match the Design v0.8 51-field contract")