diff --git a/features/domain_models.feature b/features/domain_models.feature index 503272e47..fd6b66603 100644 --- a/features/domain_models.feature +++ b/features/domain_models.feature @@ -104,3 +104,39 @@ Feature: Domain Models Validation Given I have summary counts for context updates When I create a SummaryForUpdateContextParams model Then the summary counts should match the source data + + Scenario: Create cloud billing fields from camelCase payload + Given I have sample cloud billing data + When I create a CloudBillingFields model + Then the billing fields should expose decimal values + + Scenario: Create organization with billing fields + Given I have organization data with billing + When I create an Org domain model + Then the org should include billing configuration + + Scenario: Create a user with default plan config + Given I have user data referencing a default plan config + When I create a User model + Then the user should include the default plan config + + Scenario: Create an org role definition + Given I have an org role definition + When I create an OrgRole model + Then the org role should retain its label + + Scenario: Create an org user association + Given I have an org user association + When I create an OrgUser model + Then the org user config should be attached + + Scenario: Create an invite record with timestamps + Given I have invite metadata + When I create an Invite model + Then the invite should track acceptance timestamps + + Scenario: Create a credits transaction entry + Given I have credits transaction data + When I create a CreditsTransaction model + Then the credits transaction should track balances + diff --git a/features/steps/domain_models_steps.py b/features/steps/domain_models_steps.py index 35c12698d..f959b040c 100644 --- a/features/steps/domain_models_steps.py +++ b/features/steps/domain_models_steps.py @@ -1,6 +1,7 @@ """Step definitions for domain models testing.""" from datetime import datetime +from decimal import Decimal from pathlib import Path from behave import given, then, when @@ -11,16 +12,55 @@ from cleveragents.domain.models.aimodelsdatamodels import AIModel, Provider from cleveragents.domain.models.core import ( Change, ChangeSet, + CloudBillingFields, Context, ContextType, ContextUpdateResult, + CreditType, + CreditsTransaction, + CreditsTransactionType, + Invite, + MaxContextCount, OperationType, + Org, + OrgRole, + OrgUser, Plan, PlanStatus, Project, ProjectSettings, SummaryForUpdateContextParams, + User, ) +from cleveragents.domain.models.core.enums import ModelProvider +from cleveragents.domain.models.orguserconfig import OrgUserConfig +from cleveragents.domain.models.planconfig import PlanConfig + + +def _sample_plan_config() -> PlanConfig: + """Create a reusable PlanConfig instance for tests.""" + + return PlanConfig( + auto_mode="manual", + editor="vim", + editor_command="vim", + editor_args=[], + editor_open_manually=False, + auto_continue=False, + auto_build=False, + auto_update_context=False, + auto_context=False, + smart_context=False, + auto_apply=False, + auto_commit=False, + skip_commit=False, + can_exec=False, + auto_exec=False, + auto_debug=False, + auto_debug_tries=0, + auto_revert_on_rewind=False, + skip_changes_menu=False, + ) @given("I have valid project data") @@ -381,6 +421,255 @@ def step_stats_count_operations(context): assert "applied" in context.stats +# Domain models from cloud/organization data + + +def _sample_billing_payload() -> dict[str, object]: + """Provide representative camelCase billing payload.""" + + return { + "creditsBalance": "125.5", + "monthlyGrant": "200.0", + "autoRebuyEnabled": True, + "autoRebuyMinThreshold": "5.0", + "autoRebuyToBalance": "50.0", + "notifyThreshold": "10.0", + "maxThresholdPerMonth": "500.0", + "billingCycleStartedAt": "2025-01-01T00:00:00Z", + "changedBillingMode": False, + "trialPaid": True, + "stripeSubscriptionId": "sub-123", + "subscriptionStatus": "active", + "subscriptionPausedAt": "2025-01-10T00:00:00Z", + "stripePaymentMethod": "pm_abc", + "subscriptionActionRequired": False, + "subscriptionActionRequiredInvoiceUrl": None, + } + + +@given("I have sample cloud billing data") +def step_sample_cloud_billing(context): + """Prepare representative cloud billing data.""" + + context.cloud_billing_data = _sample_billing_payload() + + +@when("I create a CloudBillingFields model") +def step_create_billing_fields(context): + """Instantiate CloudBillingFields from camelCase payload.""" + + context.billing_fields = CloudBillingFields(**context.cloud_billing_data) + + +@then("the billing fields should expose decimal values") +def step_verify_billing_fields(context): + """Validate decimal conversion for billing fields.""" + + billing = context.billing_fields + assert billing.credits_balance == Decimal("125.5") + assert billing.monthly_grant == Decimal("200.0") + assert billing.subscription_action_required is False + + +@given("I have organization data with billing") +def step_org_with_billing(context): + """Prepare organization payload referencing billing fields.""" + + context.org_payload = { + "id": "org-1", + "name": "Example Org", + "isTrial": True, + "autoAddDomainUsers": True, + "integratedModelsMode": False, + } + context.org_billing_payload = _sample_billing_payload() + + +@when("I create an Org domain model") +def step_create_org_model(context): + """Instantiate Org with nested billing fields.""" + + billing = CloudBillingFields(**context.org_billing_payload) + context.org_model = Org(cloud_billing_fields=billing, **context.org_payload) + + +@then("the org should include billing configuration") +def step_verify_org_model(context): + """Ensure the org retains billing configuration.""" + + org = context.org_model + assert org.cloud_billing_fields is not None + assert org.cloud_billing_fields.notify_threshold == Decimal("10.0") + + +@given("I have an org role definition") +def step_org_role_definition(context): + """Set up org role payload.""" + + context.org_role_data = { + "id": "role-1", + "isDefault": True, + "label": "Owner", + "description": "Full access", + } + + +@when("I create an OrgRole model") +def step_create_org_role(context): + """Instantiate OrgRole.""" + + context.org_role = OrgRole(**context.org_role_data) + + +@then("the org role should retain its label") +def step_verify_org_role(context): + """Ensure org role values persist.""" + + assert context.org_role.label == "Owner" + assert context.org_role.is_default is True + + +@given("I have user data referencing a default plan config") +def step_user_with_plan_config(context): + """Prepare user payload referencing plan config.""" + + context.user_plan_config = _sample_plan_config() + context.user_data = { + "id": "user-1", + "name": "Casey", + "email": "casey@example.com", + "isTrial": False, + "numNonDraftPlans": 3, + } + + +@when("I create a User model") +def step_create_user_model(context): + """Instantiate User with nested plan config.""" + + context.user_model = User( + default_plan_config=context.user_plan_config, **context.user_data + ) + + +@then("the user should include the default plan config") +def step_verify_user_model(context): + """Ensure user retains plan config.""" + + assert context.user_model.default_plan_config is not None + assert context.user_model.default_plan_config.auto_mode == "manual" + + +@given("I have an org user association") +def step_org_user_association(context): + """Prepare OrgUser payload.""" + + context.org_user_config = OrgUserConfig( + prompted_claude_max=False, + use_claude_subscription=True, + claude_subscription_cooldown_started_at=datetime(2025, 1, 1, 0, 0, 0), + ) + context.org_user_data = { + "orgId": "org-1", + "userId": "user-1", + "orgRoleId": "role-1", + } + + +@when("I create an OrgUser model") +def step_create_org_user(context): + """Instantiate OrgUser.""" + + context.org_user_model = OrgUser( + config=context.org_user_config, **context.org_user_data + ) + + +@then("the org user config should be attached") +def step_verify_org_user(context): + """Ensure OrgUser carries config payload.""" + + assert context.org_user_model.config is not None + assert context.org_user_model.config.use_claude_subscription is True + + +@given("I have invite metadata") +def step_invite_metadata(context): + """Prepare Invite payload.""" + + context.invite_data = { + "id": "invite-1", + "orgId": "org-1", + "email": "new@example.com", + "name": "New User", + "orgRoleId": "role-1", + "inviterId": "user-1", + "inviteeId": None, + "acceptedAt": "2025-02-01T12:00:00Z", + "createdAt": "2025-01-15T08:30:00Z", + } + + +@when("I create an Invite model") +def step_create_invite(context): + """Instantiate Invite.""" + + context.invite_model = Invite(**context.invite_data) + + +@then("the invite should track acceptance timestamps") +def step_verify_invite(context): + """Ensure Invite datetimes parsed.""" + + assert isinstance(context.invite_model.accepted_at, datetime) + assert context.invite_model.accepted_at.year == 2025 + + +@given("I have credits transaction data") +def step_credits_transaction_data(context): + """Prepare CreditsTransaction payload.""" + + context.credits_transaction_data = { + "id": "txn-1", + "orgId": "org-1", + "orgName": "Example Org", + "userId": "user-1", + "userEmail": "casey@example.com", + "userName": "Casey", + "transactionType": CreditsTransactionType.CREDIT, + "amount": "42.0", + "startBalance": "20.0", + "endBalance": "62.0", + "creditType": CreditType.TRIAL, + "creditIsAutoRebuy": True, + "creditAutoRebuyMinThreshold": "5.0", + "creditAutoRebuyToBalance": "40.0", + "debitModelProvider": ModelProvider.OPENAI, + "debitModelRole": "ModelRolePlanner", + "debitModelName": "gpt-4.1", + "debitPlanId": "plan-1", + "debitPlanName": "Core Plan", + "createdAt": "2025-01-20T00:00:00Z", + } + + +@when("I create a CreditsTransaction model") +def step_create_credits_transaction(context): + """Instantiate CreditsTransaction.""" + + context.credits_transaction = CreditsTransaction(**context.credits_transaction_data) + + +@then("the credits transaction should track balances") +def step_verify_credits_transaction(context): + """Ensure CreditsTransaction stores decimals and provider metadata.""" + + tx = context.credits_transaction + assert tx.amount == Decimal("42.0") + assert tx.debit_model_provider == ModelProvider.OPENAI + assert tx.end_balance == Decimal("62.0") + + # Auto-generated models steps @when("I import the auto-generated models") def step_import_autogenerated(context): diff --git a/implementation_plan.md b/implementation_plan.md index 47eeda913..97111805b 100644 --- a/implementation_plan.md +++ b/implementation_plan.md @@ -1558,7 +1558,15 @@ Notes: Log schema decisions, migration quirks, and adapter considerations. - Implemented the corresponding step definitions in `features/steps/architecture_steps.py` using `pkgutil.walk_packages` and `importlib.import_module`. - **Impact**: This enhances the robustness of our CI pipeline and ensures coverage metrics are accurate. All architecture tests are passing. +**2025-12-05: Org + billing domain models wired with smoke tests** + +- Completed the manual conversion of the remaining organization/billing contracts by adding `Org`, `OrgRole`, `OrgUser`, `Invite`, `OrgRole`, `User`, `CloudBillingFields`, `CreditsTransaction`, and related enums to `src/cleveragents/domain/models/core/org.py:1` and exporting them through `src/cleveragents/domain/models/core/__init__.py:1` and `src/cleveragents/domain/models/__init__.py:1` so consumers can hydrate camelCase payloads directly. +- Extended the Behave suite at `features/domain_models.feature:108` with nested-plan-config, billing, invite, org-user, and credits-transaction scenarios backed by the new step definitions in `features/steps/domain_models_steps.py:450`, covering decimal coercion, timestamp parsing, and OrgUserConfig alias handling. +- Added a Robot helper (`robot/helper_domain_models.py:1`) plus a focused suite at `robot/domain_models.robot:1` that shells out to the helper via `Run Process` to assert `org-model-ok` / `credits-model-ok` outputs; this gives us a lightweight CI guardrail for billing regressions without running all domain Behave scenarios. +- Validation evidence: `nox -s unit_tests -- features/domain_models.feature` and `nox -s integration_tests -- robot/domain_models.robot` both pass locally after the new models and suites were wired in. + **LangChain/LangGraph Integration for Phase 3:** + - Extend SQLAlchemy models to support LangGraph's state persistence schema - Implement `LangGraphStateAdapter` to bridge SQLAlchemy repositories with LangGraph checkpoints - Use LangChain's `SQLChatMessageHistory` for conversation persistence @@ -3795,7 +3803,7 @@ If you can do all of the above by end of Day 1, you're on track! - [X] Convert data_models.py conversation models (7 models): CurrentStage, ConvoMessageFlags, Subtask, ConvoMessage, ConvoSummary, TellStage, PlanningPhase — implemented in `src/cleveragents/domain/models/conversation/__init__.py` - [X] Convert data_models.py plan file models (7 models): Branch, Replacement, PlanFileResult, CurrentPlanFiles, PlanApply, CurrentPlanState, PlanStateStatus — implemented in `src/cleveragents/domain/models/planfiles/__init__.py` - [X] Convert data_models.py core stubs (6 models): Project, Plan, Context, Operation, PlanBuild, PlanResult — already exist in `src/cleveragents/domain/models/core/` - - [ ] Convert data_models.py cloud/billing models (7 models): Org, User, OrgUser, Invite, OrgRole, CloudBillingFields, CreditsTransaction — deferred, not needed for standalone mode + - [X] Convert data_models.py cloud/billing models (7 models): Org, User, OrgUser, Invite, OrgRole, CloudBillingFields, CreditsTransaction — implemented in `src/cleveragents/domain/models/core/org.py:1` with exports from `core/__init__.py:1` and `domain/models/__init__.py:1`, plus Behave coverage in `features/domain_models.feature:108` and Robot smoke tests in `robot/domain_models.robot:1`. - [X] Convert plan_model_settings.py (1 model): PlanSettings — implemented in `src/cleveragents/domain/models/plansettings/__init__.py` with proper type hints and Pydantic validation. - [ ] Convert req_res.py (53 API models) - defer to Phase 5 when implementing server endpoints - [ ] CreateEmailVerificationRequest @@ -3882,11 +3890,12 @@ If you can do all of the above by end of Day 1, you're on track! - [X] Test context-load with real files - [X] Test database persistence - [X] Ensure >85% coverage maintained + - [X] Added domain-model Robot smoke suite (`robot/domain_models.robot:1`) backed by `robot/helper_domain_models.py:1` to assert org + billing hydration without running the full Behave suite. - [X] Tests: **LangChain/LangGraph Testing Tasks** (COMPLETED 2025-11-22) - - [X] Convert existing mock to FakeListLLM (Already done in langchain_mock_provider.py) - - [X] Update MockAIProvider to use LangChain (Completed with FakeListLLM implementation) - - [X] Ensure all tests still pass (Verified - 95% coverage maintained) - - [X] Remove hardcoded responses (Using FakeListLLM response list) + - [X] Convert existing mock to FakeListLLM (Already done in langchain_mock_provider.py) + - [X] Update MockAIProvider to use LangChain (Completed with FakeListLLM implementation) + - [X] Ensure all tests still pass (Verified - 95% coverage maintained) + - [X] Remove hardcoded responses (Using FakeListLLM response list) - [X] Add LangGraph workflow tests (features/plan_generation_langgraph_coverage.feature - 17 scenarios) - [X] Test PlanGenerationGraph execution (covered in langgraph coverage tests) - [X] Test conditional edges and retry logic (should_retry scenarios in langgraph tests) diff --git a/robot/domain_models.robot b/robot/domain_models.robot new file mode 100644 index 000000000..fb2e2a0a8 --- /dev/null +++ b/robot/domain_models.robot @@ -0,0 +1,21 @@ +*** Settings *** +Documentation Smoke tests for domain model helpers +Resource common.resource +Suite Setup Setup Test Environment +Suite Teardown Cleanup Test Environment + +*** Variables *** +${HELPER_SCRIPT} robot/helper_domain_models.py + +*** Test Cases *** +Org Domain Model Hydrates Billing + [Documentation] Ensure Org + CloudBillingFields instantiate via helper script + ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} org cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} org-model-ok + +Credits Transaction Domain Model Hydrates + [Documentation] Ensure CreditsTransaction instantiates via helper script + ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} credits cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} credits-model-ok diff --git a/robot/helper_domain_models.py b/robot/helper_domain_models.py new file mode 100644 index 000000000..8b327f3af --- /dev/null +++ b/robot/helper_domain_models.py @@ -0,0 +1,88 @@ +"""Helper utilities for domain model Robot tests.""" + +from __future__ import annotations + +from datetime import datetime, timezone +from decimal import Decimal +import sys + +from cleveragents.domain.models.core.org import ( + CloudBillingFields, + CreditsTransaction, + CreditsTransactionType, + Org, +) +from cleveragents.domain.models.core.enums import ModelProvider +from cleveragents.domain.models.core.org import CreditType + + +def _org_test() -> None: + billing = CloudBillingFields( + credits_balance=Decimal("42.5"), + monthly_grant=Decimal("10.0"), + auto_rebuy_enabled=True, + auto_rebuy_min_threshold=Decimal("5.0"), + auto_rebuy_to_balance=Decimal("15.0"), + notify_threshold=Decimal("2.0"), + max_threshold_per_month=Decimal("100.0"), + billing_cycle_started_at=datetime(2025, 1, 1, tzinfo=timezone.utc), + changed_billing_mode=False, + trial_paid=False, + stripe_subscription_id="sub-123", + subscription_status="active", + subscription_paused_at=None, + stripe_payment_method="pm_123", + subscription_action_required=False, + subscription_action_required_invoice_url=None, + ) + org = Org( + id="org-robot", + name="Robot Org", + is_trial=True, + auto_add_domain_users=True, + integrated_models_mode=None, + cloud_billing_fields=billing, + ) + assert org.cloud_billing_fields is not None + assert org.cloud_billing_fields.credits_balance == Decimal("42.5") + print("org-model-ok") + + +def _credits_test() -> None: + tx = CreditsTransaction( + id="txn-robot", + org_id="org-robot", + org_name="Robot Org", + user_id="user-robot", + user_email="robot@example.com", + user_name="Robot", + transaction_type=CreditsTransactionType.CREDIT, + amount=Decimal("10.0"), + start_balance=Decimal("5.0"), + end_balance=Decimal("15.0"), + credit_type=CreditType.TRIAL, + credit_is_auto_rebuy=False, + debit_model_provider=ModelProvider.OPENAI, + debit_model_name="gpt-4.1", + debit_model_role="ModelRolePlanner", + created_at=datetime(2025, 1, 2, tzinfo=timezone.utc), + ) + assert tx.debit_model_provider == ModelProvider.OPENAI + assert tx.amount == Decimal("10.0") + print("credits-model-ok") + + +def main() -> None: + if len(sys.argv) < 2: + raise SystemExit("Expected command argument") + command = sys.argv[1] + if command == "org": + _org_test() + elif command == "credits": + _credits_test() + else: + raise SystemExit(f"Unknown helper command: {command}") + + +if __name__ == "__main__": + main() diff --git a/src/cleveragents/domain/models/__init__.py b/src/cleveragents/domain/models/__init__.py index f2f026939..f2f94f282 100644 --- a/src/cleveragents/domain/models/__init__.py +++ b/src/cleveragents/domain/models/__init__.py @@ -12,13 +12,21 @@ from .conversation import ( from .core import ( Change, ChangeSet, + CloudBillingFields, Context, ContextFile, ContextType, ContextUpdateResult, + CreditType, + CreditsTransaction, + CreditsTransactionType, + Invite, MaxContextCount, Operation, OperationType, + Org, + OrgRole, + OrgUser, Plan, PlanBuild, PlanResult, @@ -27,6 +35,7 @@ from .core import ( ProjectSettings, ProjectStats, SummaryForUpdateContextParams, + User, ) from .planfiles import ( Branch, @@ -72,4 +81,13 @@ __all__ = [ "Subtask", "SummaryForUpdateContextParams", "TellStage", + "CloudBillingFields", + "CreditType", + "CreditsTransaction", + "CreditsTransactionType", + "Invite", + "Org", + "OrgRole", + "OrgUser", + "User", ] diff --git a/src/cleveragents/domain/models/core/__init__.py b/src/cleveragents/domain/models/core/__init__.py index 15f783fe3..ff68f41ce 100644 --- a/src/cleveragents/domain/models/core/__init__.py +++ b/src/cleveragents/domain/models/core/__init__.py @@ -11,6 +11,17 @@ from .context import ( ) from .debug_attempt import DebugAttempt from .plan import Plan, PlanBuild, PlanResult, PlanStatus +from .org import ( + CloudBillingFields, + CreditType, + CreditsTransaction, + CreditsTransactionType, + Invite, + Org, + OrgRole, + OrgUser, + User, +) from .project import Project, ProjectSettings, ProjectStats __all__ = [ @@ -32,4 +43,13 @@ __all__ = [ "ProjectSettings", "ProjectStats", "SummaryForUpdateContextParams", + "CloudBillingFields", + "CreditType", + "CreditsTransaction", + "CreditsTransactionType", + "Invite", + "Org", + "OrgRole", + "OrgUser", + "User", ] diff --git a/src/cleveragents/domain/models/core/org.py b/src/cleveragents/domain/models/core/org.py new file mode 100644 index 000000000..5cbcbbb9e --- /dev/null +++ b/src/cleveragents/domain/models/core/org.py @@ -0,0 +1,226 @@ +"""Organization and billing domain models for CleverAgents. + +Based on Phase 0 discovery stubs in ``docs/reference/contracts/stubs/data_models.py`` +with additional validation rules from ADR-004 (Pydantic Validation). +""" + +from __future__ import annotations + +from datetime import datetime +from decimal import Decimal +from enum import Enum + +from pydantic import BaseModel, ConfigDict, Field + +from ..orguserconfig.org_user_config import OrgUserConfig +from ..planconfig.plan_config import PlanConfig +from .enums import ModelProvider + +__all__ = [ + "CloudBillingFields", + "CreditType", + "CreditsTransaction", + "CreditsTransactionType", + "Invite", + "Org", + "OrgRole", + "OrgUser", + "User", +] + + +class OrgRole(BaseModel): + """Organization role metadata.""" + + id: str = Field(..., description="Role identifier") + is_default: bool = Field(..., alias="isDefault") + label: str = Field(..., min_length=1) + description: str = Field(..., min_length=1) + + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True, + populate_by_name=True, + ) + + +class CloudBillingFields(BaseModel): + """Billing configuration for a CleverAgents organization.""" + + credits_balance: Decimal = Field(..., alias="creditsBalance") + monthly_grant: Decimal = Field(..., alias="monthlyGrant") + auto_rebuy_enabled: bool = Field(..., alias="autoRebuyEnabled") + auto_rebuy_min_threshold: Decimal = Field(..., alias="autoRebuyMinThreshold") + auto_rebuy_to_balance: Decimal = Field(..., alias="autoRebuyToBalance") + notify_threshold: Decimal = Field(..., alias="notifyThreshold") + max_threshold_per_month: Decimal = Field(..., alias="maxThresholdPerMonth") + billing_cycle_started_at: datetime = Field(..., alias="billingCycleStartedAt") + changed_billing_mode: bool = Field(..., alias="changedBillingMode") + trial_paid: bool = Field(..., alias="trialPaid") + stripe_subscription_id: str | None = Field( + default=None, alias="stripeSubscriptionId" + ) + subscription_status: str | None = Field(default=None, alias="subscriptionStatus") + subscription_paused_at: datetime | None = Field( + default=None, alias="subscriptionPausedAt" + ) + stripe_payment_method: str | None = Field(default=None, alias="stripePaymentMethod") + subscription_action_required: bool = Field(..., alias="subscriptionActionRequired") + subscription_action_required_invoice_url: str | None = Field( + default=None, alias="subscriptionActionRequiredInvoiceUrl" + ) + + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True, + arbitrary_types_allowed=False, + populate_by_name=True, + ) + + +class Org(BaseModel): + """Organization profile.""" + + id: str = Field(..., description="Organization identifier") + name: str = Field(..., min_length=1) + is_trial: bool = Field(..., alias="isTrial") + auto_add_domain_users: bool = Field(..., alias="autoAddDomainUsers") + integrated_models_mode: bool | None = Field( + default=None, alias="integratedModelsMode" + ) + cloud_billing_fields: CloudBillingFields | None = Field( + default=None, alias="cloudBillingFields" + ) + + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True, + populate_by_name=True, + ) + + +class User(BaseModel): + """User profile within an organization.""" + + id: str = Field(...) + name: str = Field(..., min_length=1) + email: str = Field(..., min_length=3) + is_trial: bool = Field(..., alias="isTrial") + num_non_draft_plans: int = Field(..., alias="numNonDraftPlans", ge=0) + default_plan_config: PlanConfig | None = Field( + default=None, alias="defaultPlanConfig" + ) + + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True, + populate_by_name=True, + ) + + +class OrgUser(BaseModel): + """Association between a user and an organization.""" + + org_id: str = Field(..., alias="orgId") + user_id: str = Field(..., alias="userId") + org_role_id: str = Field(..., alias="orgRoleId") + config: OrgUserConfig | None = Field(default=None) + + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True, + populate_by_name=True, + ) + + +class Invite(BaseModel): + """Organization invitation record.""" + + id: str = Field(...) + org_id: str = Field(..., alias="orgId") + email: str = Field(...) + name: str = Field(...) + org_role_id: str = Field(..., alias="orgRoleId") + inviter_id: str = Field(..., alias="inviterId") + invitee_id: str | None = Field(default=None, alias="inviteeId") + accepted_at: datetime | None = Field(default=None, alias="acceptedAt") + created_at: datetime = Field(..., alias="createdAt") + + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True, + populate_by_name=True, + ) + + +class CreditsTransactionType(str, Enum): + """Type of credits transaction.""" + + CREDIT = "CreditsTransactionTypeCredit" + DEBIT = "CreditsTransactionTypeDebit" + + +class CreditType(str, Enum): + """Categorization for credits changes.""" + + TRIAL = "CreditTypeTrial" + GRANT = "CreditTypeGrant" + ADMIN_GRANT = "CreditTypeAdminGrant" + PURCHASE = "CreditTypePurchase" + SWITCH = "CreditTypeSwitch" + + +class CreditsTransaction(BaseModel): + """Ledger entry for credits usage or replenishment.""" + + id: str = Field(...) + org_id: str = Field(..., alias="orgId") + org_name: str = Field(..., alias="orgName") + user_id: str | None = Field(default=None, alias="userId") + user_email: str | None = Field(default=None, alias="userEmail") + user_name: str | None = Field(default=None, alias="userName") + transaction_type: CreditsTransactionType = Field(..., alias="transactionType") + amount: Decimal = Field(...) + start_balance: Decimal = Field(..., alias="startBalance") + end_balance: Decimal = Field(..., alias="endBalance") + credit_type: CreditType | None = Field(default=None, alias="creditType") + credit_is_auto_rebuy: bool = Field(..., alias="creditIsAutoRebuy") + credit_auto_rebuy_min_threshold: Decimal | None = Field( + default=None, alias="creditAutoRebuyMinThreshold" + ) + credit_auto_rebuy_to_balance: Decimal | None = Field( + default=None, alias="creditAutoRebuyToBalance" + ) + debit_input_tokens: int | None = Field(default=None, alias="debitInputTokens") + debit_output_tokens: int | None = Field(default=None, alias="debitOutputTokens") + debit_model_input_price_per_token: Decimal | None = Field( + default=None, alias="debitModelInputPricePerToken" + ) + debit_model_output_price_per_token: Decimal | None = Field( + default=None, alias="debitModelOutputPricePerToken" + ) + debit_base_amount: Decimal | None = Field(default=None, alias="debitBaseAmount") + debit_surcharge: Decimal | None = Field(default=None, alias="debitSurcharge") + debit_model_provider: ModelProvider | None = Field( + default=None, alias="debitModelProvider" + ) + debit_model_name: str | None = Field(default=None, alias="debitModelName") + debit_model_pack_name: str | None = Field(default=None, alias="debitModelPackName") + debit_model_role: str | None = Field(default=None, alias="debitModelRole") + debit_purpose: str | None = Field(default=None, alias="debitPurpose") + debit_plan_id: str | None = Field(default=None, alias="debitPlanId") + debit_plan_name: str | None = Field(default=None, alias="debitPlanName") + debit_id: str | None = Field(default=None, alias="debitId") + debit_cache_discount: Decimal | None = Field( + default=None, alias="debitCacheDiscount" + ) + debit_session_id: str | None = Field(default=None, alias="debitSessionId") + created_at: datetime = Field(..., alias="createdAt") + + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True, + arbitrary_types_allowed=False, + populate_by_name=True, + use_enum_values=True, + )