ipulse-shared-core-ftredge 19.0.1__tar.gz → 22.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ipulse-shared-core-ftredge might be problematic. Click here for more details.

Files changed (70) hide show
  1. {ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge.egg-info → ipulse_shared_core_ftredge-22.1.1}/PKG-INFO +3 -4
  2. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/setup.py +3 -4
  3. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/cache/shared_cache.py +1 -2
  4. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +4 -4
  5. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/exceptions/base_exceptions.py +23 -0
  6. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/models/__init__.py +3 -7
  7. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/models/base_data_model.py +17 -19
  8. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/catalog/__init__.py +10 -0
  9. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py +273 -0
  10. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/catalog/usertype.py +170 -0
  11. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/user/__init__.py +5 -0
  12. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/user/user_permissions.py +66 -0
  13. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/models/subscription.py → ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/user/user_subscription.py +66 -20
  14. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/models/user_auth.py → ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/user/userauth.py +19 -10
  15. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/models/user_profile.py → ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/user/userprofile.py +53 -21
  16. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/user/userstatus.py +430 -0
  17. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/monitoring/__init__.py +5 -0
  18. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/monitoring/tracemon.py +320 -0
  19. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/__init__.py +23 -0
  20. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/services/base/__init__.py +3 -1
  21. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/services/base/base_firestore_service.py +73 -14
  22. {ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services → ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/base}/cache_aware_firestore_service.py +46 -32
  23. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/catalog/__init__.py +14 -0
  24. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py +273 -0
  25. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py +307 -0
  26. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/services/charging_processors.py +25 -25
  27. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user/__init__.py +17 -0
  28. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user/firebase_auth_admin_helpers.py +160 -0
  29. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user/user_core_service.py +559 -0
  30. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +726 -0
  31. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user/user_permissions_operations.py +392 -0
  32. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user/user_subscription_operations.py +484 -0
  33. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user/userauth_operations.py +928 -0
  34. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user/userprofile_operations.py +166 -0
  35. ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user/userstatus_operations.py +212 -0
  36. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/charging_service.py → ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user_charging_service.py +9 -9
  37. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge.egg-info}/PKG-INFO +3 -4
  38. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge.egg-info/SOURCES.txt +22 -13
  39. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +1 -2
  40. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/models/user_status.py +0 -495
  41. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/monitoring/__init__.py +0 -5
  42. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/monitoring/microservmon.py +0 -483
  43. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/__init__.py +0 -25
  44. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/__init__.py +0 -37
  45. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/iam_management_operations.py +0 -326
  46. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/subscription_management_operations.py +0 -384
  47. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/user_account_operations.py +0 -479
  48. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/user_auth_operations.py +0 -305
  49. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/user_core_service.py +0 -651
  50. ipulse_shared_core_ftredge-19.0.1/src/ipulse_shared_core_ftredge/services/user/user_holistic_operations.py +0 -436
  51. ipulse_shared_core_ftredge-19.0.1/tests/test_cache_aware_service.py +0 -270
  52. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/LICENCE +0 -0
  53. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/README.md +0 -0
  54. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/pyproject.toml +0 -0
  55. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/setup.cfg +0 -0
  56. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/__init__.py +0 -0
  57. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/cache/__init__.py +0 -0
  58. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
  59. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +0 -0
  60. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_protected_router.py +0 -0
  61. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/dependencies/firestore_client.py +0 -0
  62. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/exceptions/__init__.py +0 -0
  63. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/exceptions/user_exceptions.py +0 -0
  64. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/models/base_api_response.py +0 -0
  65. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -0
  66. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/utils/custom_json_encoder.py +0 -0
  67. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/utils/json_encoder.py +0 -0
  68. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge.egg-info/dependency_links.txt +0 -0
  69. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge.egg-info/top_level.txt +0 -0
  70. {ipulse_shared_core_ftredge-19.0.1 → ipulse_shared_core_ftredge-22.1.1}/tests/test_shared_cache.py +0 -0
@@ -1,19 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipulse_shared_core_ftredge
3
- Version: 19.0.1
3
+ Version: 22.1.1
4
4
  Summary: Shared Core models and Logger util for the Pulse platform project. Using AI for financial advisory and investment management.
5
5
  Home-page: https://github.com/TheFutureEdge/ipulse_shared_core
6
6
  Author: Russlan Ramdowar
7
7
  Classifier: Programming Language :: Python :: 3
8
8
  Classifier: License :: OSI Approved :: MIT License
9
9
  Classifier: Operating System :: OS Independent
10
- Requires-Python: >=3.11
10
+ Requires-Python: >=3.12
11
11
  License-File: LICENCE
12
12
  Requires-Dist: pydantic[email]~=2.5
13
13
  Requires-Dist: python-dateutil~=2.8
14
14
  Requires-Dist: fastapi~=0.115.8
15
- Requires-Dist: pytest
16
- Requires-Dist: ipulse_shared_base_ftredge==7.2.0
15
+ Requires-Dist: ipulse_shared_base_ftredge==10.2.1
17
16
  Dynamic: author
18
17
  Dynamic: classifier
19
18
  Dynamic: home-page
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
3
3
 
4
4
  setup(
5
5
  name='ipulse_shared_core_ftredge',
6
- version='19.0.1',
6
+ version='22.1.1',
7
7
  package_dir={'': 'src'}, # Specify the source directory
8
8
  packages=find_packages(where='src'), # Look for packages in 'src'
9
9
  install_requires=[
@@ -11,8 +11,7 @@ setup(
11
11
  'pydantic[email]~=2.5',
12
12
  'python-dateutil~=2.8',
13
13
  'fastapi~=0.115.8',
14
- 'pytest',
15
- 'ipulse_shared_base_ftredge==7.2.0',
14
+ 'ipulse_shared_base_ftredge==10.2.1',
16
15
  ],
17
16
  author='Russlan Ramdowar',
18
17
  description='Shared Core models and Logger util for the Pulse platform project. Using AI for financial advisory and investment management.',
@@ -23,5 +22,5 @@ setup(
23
22
  'License :: OSI Approved :: MIT License',
24
23
  'Operating System :: OS Independent',
25
24
  ],
26
- python_requires='>=3.11',
25
+ python_requires='>=3.12',
27
26
  )
@@ -1,12 +1,11 @@
1
1
  """Module for shared caching functionality that can be used across microservices."""
2
- import os
3
2
  import time
4
3
  import logging
5
4
  import traceback
6
5
  import inspect
7
6
  import asyncio
8
7
  import threading
9
- from typing import Dict, Any, Optional, TypeVar, Generic, Callable, Tuple, List, Awaitable
8
+ from typing import Dict, Any, Optional, TypeVar, Generic, Callable, Tuple,Awaitable
10
9
 
11
10
  T = TypeVar('T')
12
11
 
@@ -9,7 +9,7 @@ import json
9
9
  import httpx
10
10
  from fastapi import HTTPException, Request
11
11
  from google.cloud import firestore
12
- from ipulse_shared_core_ftredge.services import ServiceError, AuthorizationError, ResourceNotFoundError
12
+ from ipulse_shared_core_ftredge.exceptions import ServiceError, AuthorizationError, ResourceNotFoundError
13
13
  from ipulse_shared_core_ftredge.models import UserStatus
14
14
  from ipulse_shared_core_ftredge.utils.json_encoder import convert_to_json_serializable
15
15
 
@@ -278,8 +278,8 @@ async def authorizeAPIRequest(
278
278
  primary_usertype = userstatus.get("primary_usertype")
279
279
  secondary_usertypes = userstatus.get("secondary_usertypes", [])
280
280
 
281
- # Extract IAM domain permissions
282
- iam_domain_permissions = userstatus.get("iam_domain_permissions", {})
281
+ # Extract IAM permissions
282
+ iam_permissions = userstatus.get("iam_permissions", {})
283
283
 
284
284
  # Format the authz_input to match what the OPA policies expect
285
285
  authz_input = {
@@ -290,7 +290,7 @@ async def authorizeAPIRequest(
290
290
  "secondary_usertypes": secondary_usertypes,
291
291
  "usertypes": [primary_usertype] + secondary_usertypes if primary_usertype else secondary_usertypes,
292
292
  "email_verified": request.state.user.get("email_verified", False),
293
- "iam_domain_permissions": iam_domain_permissions,
293
+ "iam_permissions": iam_permissions,
294
294
  "sbscrptn_based_insight_credits": userstatus.get("sbscrptn_based_insight_credits", 0),
295
295
  "extra_insight_credits": userstatus.get("extra_insight_credits", 0)
296
296
  },
@@ -133,3 +133,26 @@ class ValidationError(BaseServiceException):
133
133
  resource_id=resource_id,
134
134
  additional_info=additional_info
135
135
  )
136
+
137
+
138
+ class ConfigurationError(BaseServiceException):
139
+ def __init__(
140
+ self,
141
+ detail: str,
142
+ resource_type: str = "configuration",
143
+ resource_id: Optional[str] = None,
144
+ operation: Optional[str] = None,
145
+ additional_info: Optional[Dict[str, Any]] = None,
146
+ original_error: Optional[Exception] = None
147
+ ):
148
+ if operation:
149
+ detail = f"{detail} (Operation: {operation})"
150
+
151
+ super().__init__(
152
+ status_code=500,
153
+ detail=detail,
154
+ resource_type=resource_type,
155
+ resource_id=resource_id,
156
+ additional_info=additional_info,
157
+ original_error=original_error
158
+ )
@@ -1,8 +1,4 @@
1
- from .user_profile import UserProfile
2
- from .subscription import Subscription
3
- from .user_status import UserStatus, IAMUnitRefAssignment
4
- from .user_auth import UserAuth
5
- from .base_api_response import BaseAPIResponse , CustomJSONResponse, CreditChargeableAPIResponse, UserCreditBalance, UpdatedUserCreditInfo
6
1
  from .base_data_model import BaseDataModel
7
-
8
-
2
+ from .base_api_response import BaseAPIResponse , CustomJSONResponse, CreditChargeableAPIResponse, UserCreditBalance, UpdatedUserCreditInfo
3
+ from .user import UserProfile, UserSubscription, UserStatus, UserAuth, UserPermission
4
+ from .catalog import SubscriptionPlan, ProrationMethod, PlanUpgradePath, UserType
@@ -1,7 +1,6 @@
1
1
  from datetime import datetime, timezone
2
2
  from typing import Any
3
3
  from typing import ClassVar
4
- from typing import Optional, Dict
5
4
  from pydantic import BaseModel, Field, ConfigDict, field_validator
6
5
  import dateutil.parser
7
6
 
@@ -21,9 +20,9 @@ class BaseDataModel(BaseModel):
21
20
  frozen=True # Keep schema version frozen for data integrity
22
21
  )
23
22
 
24
- # Audit fields - now mutable for updates
25
- created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
26
- created_by: str = Field(...)
23
+ # Audit fields - created fields are frozen after creation, updated fields are mutable
24
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), frozen=True)
25
+ created_by: str = Field(..., frozen=True)
27
26
  updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
28
27
  updated_by: str = Field(...)
29
28
 
@@ -35,22 +34,21 @@ class BaseDataModel(BaseModel):
35
34
  @field_validator('created_at', 'updated_at', mode='before')
36
35
  @classmethod
37
36
  def parse_datetime(cls, v: Any) -> datetime:
38
- if isinstance(v, datetime): # If Firestore already gave a datetime object
39
- return v # Just use it, no parsing needed
40
- if isinstance(v, str): # If it's a string (e.g. from an API request, not Firestore direct)
37
+ """
38
+ Ensures that datetime fields are properly parsed into datetime objects.
39
+ Handles both datetime objects (from Firestore) and ISO format strings (from APIs).
40
+ """
41
+ if isinstance(v, datetime):
42
+ # If it's already a datetime object (including Firestore's DatetimeWithNanoseconds),
43
+ # return it directly.
44
+ return v
45
+
46
+ if isinstance(v, str):
47
+ # If it's a string, parse it into a datetime object.
41
48
  try:
42
49
  return dateutil.parser.isoparse(v)
43
50
  except (TypeError, ValueError) as e:
44
51
  raise ValueError(f"Invalid datetime string format: {v} - {e}")
45
- # Firestore might send google.api_core.datetime_helpers.DatetimeWithNanoseconds
46
- # which is a subclass of datetime.datetime, so isinstance(v, datetime) should catch it.
47
- # If for some reason it's a different type not caught by isinstance(v, datetime)
48
- # but has isoformat(), perhaps try that, but it's unlikely with current Firestore client.
49
- # For example, if v is some custom timestamp object from an older library:
50
- if hasattr(v, 'isoformat'): # Fallback for unknown datetime-like objects
51
- try:
52
- return dateutil.parser.isoparse(v.isoformat())
53
- except Exception as e:
54
- raise ValueError(f"Could not parse datetime-like object: {v} - {e}")
55
-
56
- raise ValueError(f"Unsupported type for datetime parsing: {type(v)} value: {v}")
52
+
53
+ # If the type is not a datetime or a string, it's an unsupported format.
54
+ raise ValueError(f"Unsupported type for datetime parsing: {type(v)}")
@@ -0,0 +1,10 @@
1
+ """
2
+ Models for default configurations
3
+ """
4
+
5
+ from .subscriptionplan import SubscriptionPlan, ProrationMethod, PlanUpgradePath
6
+ from .usertype import UserType
7
+ __all__ = [
8
+ "UserType",
9
+ "SubscriptionPlan"
10
+ ]
@@ -0,0 +1,273 @@
1
+ """
2
+ Subscription Plan Defaults Model
3
+
4
+ This module defines the configuration templates for subscription plans that are stored in Firestore.
5
+ These templates are used to create actual user subscriptions with consistent settings.
6
+ """
7
+
8
+ from typing import Dict, Any, Optional, ClassVar, List
9
+ from enum import StrEnum
10
+ from pydantic import Field, ConfigDict, field_validator,model_validator, BaseModel
11
+ from ipulse_shared_base_ftredge import (Layer, Module, list_enums_as_lower_strings,
12
+ Subject, SubscriptionPlanName,ObjectOverallStatus,
13
+ SubscriptionStatus, TimeUnit)
14
+ from ..base_data_model import BaseDataModel
15
+ from ..user.user_permissions import UserPermission
16
+
17
+
18
+ class ProrationMethod(StrEnum):
19
+ """Methods for handling proration when upgrading plans."""
20
+ IMMEDIATE = "immediate"
21
+ PRORATED = "prorated"
22
+ END_OF_CYCLE = "end_of_cycle"
23
+
24
+
25
+ class Proration(BaseModel):
26
+ """Defines the proration behavior for subscription changes."""
27
+ pro_rata_billing: bool = Field(
28
+ default=True,
29
+ description="If true, charge a pro-rated amount for the remaining time in the current billing cycle."
30
+ )
31
+ proration_date: Optional[int] = Field(
32
+ default=None,
33
+ description="The specific date to use for proration calculations, if applicable."
34
+ )
35
+
36
+
37
+ class PlanUpgradePath(BaseModel):
38
+ """Represents an upgrade path from a source plan to the plan where this path is defined."""
39
+
40
+ price_usd: float = Field(
41
+ ...,
42
+ ge=0,
43
+ description="Price for upgrading to this plan in USD"
44
+ )
45
+
46
+ proration_method: ProrationMethod = Field(
47
+ ...,
48
+ description="How to handle proration when upgrading"
49
+ )
50
+
51
+
52
+ ############################################ !!!!! ALWAYS UPDATE SCHEMA VERSION IF SCHEMA IS BEING MODIFIED !!! ############################################
53
+ class SubscriptionPlan(BaseDataModel):
54
+ """
55
+ Configuration template for subscription plans stored in Firestore.
56
+ These templates define the default settings applied when creating user subscriptions.
57
+ """
58
+
59
+ model_config = ConfigDict(extra="forbid")
60
+
61
+ VERSION: ClassVar[float] = 1.0
62
+ DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE, Subject.CATALOG))
63
+ OBJ_REF: ClassVar[str] = "subscriptionplan"
64
+
65
+ # System-managed fields
66
+ schema_version: float = Field(
67
+ default=VERSION,
68
+ description="Version of this Class == version of DB Schema",
69
+ frozen=True
70
+ )
71
+
72
+ id: Optional[str] = Field(
73
+ default=None,
74
+ description="Unique identifier for this plan template (e.g., 'free_subscription_1'). Auto-generated if not provided.",
75
+ frozen=True
76
+ )
77
+
78
+ plan_name: SubscriptionPlanName = Field(
79
+ ...,
80
+ description="Subscription plan type (FREE, BASE, PREMIUM)",
81
+ frozen=True
82
+ )
83
+
84
+ plan_version: int = Field(
85
+ ...,
86
+ ge=1,
87
+ description="Version of this plan template",
88
+ frozen=True
89
+ )
90
+
91
+ pulse_status: ObjectOverallStatus = Field(
92
+ default=ObjectOverallStatus.ACTIVE,
93
+ description="Overall status of this subscription plan configuration"
94
+ )
95
+
96
+ # Display information
97
+ display_name: str = Field(
98
+ ...,
99
+ min_length=1,
100
+ description="Human-readable plan name",
101
+ frozen=True
102
+ )
103
+
104
+ description: str = Field(
105
+ ...,
106
+ min_length=1,
107
+ description="Description of what this plan includes",
108
+ frozen=True
109
+ )
110
+
111
+ granted_iam_permissions: List[UserPermission] = Field(
112
+ default_factory=list,
113
+ description="List of all IAM permission granted by this plan",
114
+ frozen=True
115
+ )
116
+
117
+ # Credit configuration
118
+ subscription_based_insight_credits_per_update: int = Field(
119
+ ...,
120
+ ge=0,
121
+ description="Number of insight credits added per update cycle",
122
+ frozen=True
123
+ )
124
+
125
+ subscription_based_insight_credits_update_freq_h: int = Field(
126
+ ...,
127
+ gt=0,
128
+ description="How often insight credits are updated (in hours)",
129
+ frozen=True
130
+ )
131
+
132
+ extra_insight_credits_per_cycle: int = Field(
133
+ ...,
134
+ ge=0,
135
+ description="Bonus insight credits granted per subscription cycle",
136
+ frozen=True
137
+ )
138
+
139
+ voting_credits_per_update: int = Field(
140
+ ...,
141
+ ge=0,
142
+ description="Number of voting credits added per update cycle",
143
+ frozen=True
144
+ )
145
+
146
+ voting_credits_update_freq_h: int = Field(
147
+ ...,
148
+ gt=0,
149
+ description="How often voting credits are updated (in hours)",
150
+ frozen=True
151
+ )
152
+
153
+ # Plan cycle configuration
154
+ plan_validity_cycle_length: int = Field(
155
+ ...,
156
+ gt=0,
157
+ description="Length of each subscription cycle (e.g., 1, 3, 12)",
158
+ frozen=True
159
+ )
160
+
161
+ plan_validity_cycle_unit: TimeUnit = Field(
162
+ ...,
163
+ description="Unit for the cycle length (month, year, etc.)",
164
+ frozen=True
165
+ )
166
+
167
+ # Pricing
168
+ plan_per_cycle_price_usd: float = Field(
169
+ ...,
170
+ ge=0,
171
+ description="Price per subscription cycle in USD",
172
+ frozen=True
173
+ )
174
+
175
+ # Features and customization
176
+ plan_extra_features: Dict[str, Any] = Field(
177
+ default_factory=dict,
178
+ description="Additional features enabled by this plan",
179
+ frozen=True
180
+ )
181
+
182
+ # Upgrade paths
183
+ plan_upgrade_paths: Dict[str, PlanUpgradePath] = Field(
184
+ default_factory=dict,
185
+ description="Defines valid upgrade paths TO this plan FROM other plans (source_plan_id -> upgrade_details)",
186
+ frozen=True
187
+ )
188
+
189
+ # Default settings
190
+ plan_default_auto_renewal: bool = Field(
191
+ ...,
192
+ description="Default auto-renewal setting for new subscriptions",
193
+ frozen=True
194
+ )
195
+
196
+ plan_default_status: SubscriptionStatus = Field(
197
+ ...,
198
+ description="Default status for new subscriptions with this plan",
199
+ frozen=True
200
+ )
201
+
202
+ # Fallback configuration
203
+ fallback_plan_id_if_current_plan_expired: Optional[str] = Field(
204
+ ...,
205
+ description="Plan to fall back to when this plan expires (None for no fallback)",
206
+ frozen=True
207
+ )
208
+
209
+ @model_validator(mode='before')
210
+ @classmethod
211
+ def set_id_if_not_provided(cls, data: Any) -> Any:
212
+ """Generate an ID from plan_name and plan_version if not provided."""
213
+ if isinstance(data, dict):
214
+ plan_name = data.get('plan_name')
215
+ plan_version = data.get('plan_version')
216
+ provided_id = data.get('id')
217
+
218
+ if plan_name and plan_version is not None:
219
+ plan_name_str = str(plan_name)
220
+ expected_id = f"{plan_name_str}_{plan_version}"
221
+
222
+ if provided_id is None:
223
+ # Auto-generate ID
224
+ data['id'] = expected_id
225
+ else:
226
+ # Validate provided ID matches expected format
227
+ if provided_id != expected_id:
228
+ raise ValueError(
229
+ f"Invalid ID format. Expected '{expected_id}' based on "
230
+ f"plan_name='{plan_name_str}' and plan_version={plan_version}, "
231
+ f"but got '{provided_id}'. ID must follow format: {{plan_name}}_{{plan_version}}"
232
+ )
233
+ return data
234
+
235
+
236
+ @field_validator('granted_iam_permissions')
237
+ @classmethod
238
+ def validate_iam_permissions(cls, v: List[UserPermission]) -> List[UserPermission]:
239
+ """Validate IAM permissions structure."""
240
+ if not isinstance(v, list):
241
+ raise ValueError("granted_iam_permissions must be a list")
242
+
243
+ for i, permission in enumerate(v):
244
+ if not isinstance(permission, UserPermission):
245
+ raise ValueError(f"Permission at index {i} must be a UserPermission instance")
246
+
247
+ return v
248
+
249
+ @field_validator('plan_upgrade_paths')
250
+ @classmethod
251
+ def validate_upgrade_paths(cls, v: Dict[str, PlanUpgradePath]) -> Dict[str, PlanUpgradePath]:
252
+ """Validate upgrade paths."""
253
+ for source_plan_id, upgrade_path in v.items():
254
+ if not isinstance(source_plan_id, str) or not source_plan_id.strip():
255
+ raise ValueError(f"Source plan ID must be a non-empty string, got: {source_plan_id}")
256
+
257
+ if not isinstance(upgrade_path, PlanUpgradePath):
258
+ raise ValueError(f"Upgrade path for '{source_plan_id}' must be a PlanUpgradePath instance")
259
+
260
+ return v
261
+
262
+ def get_cycle_duration_hours(self) -> int:
263
+ """Calculate the total duration of one cycle in hours."""
264
+ unit_to_hours = {
265
+ TimeUnit.MINUTE: 1/60,
266
+ TimeUnit.HOUR: 1,
267
+ TimeUnit.DAY: 24,
268
+ TimeUnit.WEEK: 24 * 7,
269
+ TimeUnit.MONTH: 24 * 30, # Approximate
270
+ TimeUnit.YEAR: 24 * 365, # Approximate
271
+ }
272
+ multiplier = unit_to_hours.get(self.plan_validity_cycle_unit, 24 * 30)
273
+ return int(self.plan_validity_cycle_length * multiplier)
@@ -0,0 +1,170 @@
1
+ """
2
+ User Defaults Model
3
+
4
+ This module defines the configuration templates for user type defaults that are stored in Firestore.
5
+ These templates are used to create user profiles and statuses with consistent default settings
6
+ based on their user type (superadmin, admin, internal, authenticated, anonymous).
7
+ """
8
+
9
+ from typing import Dict, Any, Optional, ClassVar, List
10
+ from pydantic import Field, ConfigDict, field_validator, model_validator
11
+ from ipulse_shared_base_ftredge import Layer, Module, list_enums_as_lower_strings, Subject, ObjectOverallStatus
12
+ from ipulse_shared_base_ftredge.enums.enums_iam import IAMUserType
13
+ from ipulse_shared_core_ftredge.models.base_data_model import BaseDataModel
14
+ from ipulse_shared_core_ftredge.models.user.user_permissions import UserPermission
15
+
16
+ # ORIGINAL AUTHOR ="russlan.ramdowar;russlan@ftredge.com"
17
+ # CLASS_ORIGIN_DATE="2025-06-27"
18
+
19
+
20
+ ############################################ !!!!! ALWAYS UPDATE SCHEMA VERSION IF SCHEMA IS BEING MODIFIED !!! ############################################
21
+ class UserType(BaseDataModel):
22
+ """
23
+ Configuration template for user type defaults stored in Firestore.
24
+ These templates define the default settings applied when creating users of specific types.
25
+ """
26
+
27
+ model_config = ConfigDict(extra="forbid")
28
+
29
+ VERSION: ClassVar[float] = 1.0
30
+ DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.CATALOG.name))
31
+ OBJ_REF: ClassVar[str] = "usertype"
32
+
33
+ # System-managed fields
34
+ schema_version: float = Field(
35
+ default=VERSION,
36
+ description="Version of this Class == version of DB Schema",
37
+ frozen=True
38
+ )
39
+
40
+ id: Optional[str] = Field(
41
+ default=None,
42
+ description="Unique identifier for this user type template (e.g., 'superadmin_1', 'authenticated_1'). Auto-generated if not provided.",
43
+ frozen=True
44
+ )
45
+
46
+ version: int = Field(
47
+ ...,
48
+ ge=1,
49
+ description="Version of this user type template",
50
+ frozen=True
51
+ )
52
+
53
+ pulse_status: ObjectOverallStatus = Field(
54
+ default=ObjectOverallStatus.ACTIVE,
55
+ description="Overall status of this user type configuration"
56
+ )
57
+
58
+ # User type configuration
59
+ primary_usertype: IAMUserType = Field(
60
+ ...,
61
+ description="Primary user type for this configuration template",
62
+ frozen=True
63
+ )
64
+
65
+ secondary_usertypes: List[IAMUserType] = Field(
66
+ default_factory=list,
67
+ description="Secondary user types automatically assigned to users of this primary type",
68
+ frozen=True
69
+ )
70
+
71
+ # Organization defaults
72
+ default_organizations: List[str] = Field(
73
+ default_factory=list,
74
+ description="Default organization UIDs for users of this type",
75
+ frozen=True
76
+ )
77
+
78
+ # IAM permissions structure - simplified flattened list
79
+ granted_iam_permissions: List[UserPermission] = Field(
80
+ default_factory=list,
81
+ description="Default IAM permissions granted to users of this type.",
82
+ frozen=True
83
+ )
84
+
85
+ default_extra_insight_credits: int = Field(
86
+ default=0,
87
+ ge=0,
88
+ description="Default extra insight credits for users of this type",
89
+ frozen=True
90
+ )
91
+
92
+ default_voting_credits: int = Field(
93
+ default=0,
94
+ ge=0,
95
+ description="Default voting credits for users of this type",
96
+ frozen=True
97
+ )
98
+
99
+ # Subscription defaults
100
+ default_subscription_plan_if_unpaid: Optional[str] = Field(
101
+ default=None,
102
+ description="Default subscription plan ID to assign if user has no active subscription",
103
+ frozen=True
104
+ )
105
+
106
+ # Additional metadata
107
+ metadata: Dict[str, Any] = Field(
108
+ default_factory=dict,
109
+ description="Additional metadata for this user type configuration",
110
+ frozen=True
111
+ )
112
+
113
+ @model_validator(mode='before')
114
+ @classmethod
115
+ def set_id_if_not_provided(cls, data: Any) -> Any:
116
+ """Generate an ID from primary_usertype and version if not provided."""
117
+ if isinstance(data, dict):
118
+ primary_usertype = data.get('primary_usertype')
119
+ version = data.get('version')
120
+ provided_id = data.get('id')
121
+
122
+ if primary_usertype and version is not None:
123
+ primary_usertype_str = str(primary_usertype)
124
+ expected_id = f"{primary_usertype_str}_{version}"
125
+
126
+ if provided_id is None:
127
+ # Auto-generate ID
128
+ data['id'] = expected_id
129
+ else:
130
+ # Validate provided ID matches expected format
131
+ if provided_id != expected_id:
132
+ raise ValueError(
133
+ f"Invalid ID format. Expected '{expected_id}' based on "
134
+ f"primary_usertype='{primary_usertype_str}' and version={version}, "
135
+ f"but got '{provided_id}'. ID must follow format: {{primary_usertype}}_{{version}}"
136
+ )
137
+ return data
138
+
139
+ @property
140
+ def usertype_id(self) -> str:
141
+ """Get the ID as a non-optional string. ID is always set after validation."""
142
+ if self.id is None:
143
+ raise ValueError("UserType ID is not set - this should not happen after model validation")
144
+ return self.id
145
+
146
+ @field_validator('granted_iam_permissions')
147
+ @classmethod
148
+ def validate_iam_permissions(cls, v: List[UserPermission]) -> List[UserPermission]:
149
+ """Validate IAM permissions structure."""
150
+ if not isinstance(v, list):
151
+ raise ValueError("granted_iam_permissions must be a list")
152
+
153
+ for i, permission in enumerate(v):
154
+ if not isinstance(permission, UserPermission):
155
+ raise ValueError(f"Permission at index {i} must be a UserPermission instance")
156
+
157
+ return v
158
+
159
+ @field_validator('secondary_usertypes')
160
+ @classmethod
161
+ def validate_secondary_usertypes(cls, v: List[IAMUserType]) -> List[IAMUserType]:
162
+ """Validate secondary user types list."""
163
+ # Remove duplicates while preserving order
164
+ seen = set()
165
+ unique_list = []
166
+ for user_type in v:
167
+ if user_type not in seen:
168
+ seen.add(user_type)
169
+ unique_list.append(user_type)
170
+ return unique_list
@@ -0,0 +1,5 @@
1
+ from .userprofile import UserProfile
2
+ from .user_subscription import UserSubscription
3
+ from .user_permissions import UserPermission
4
+ from .userstatus import UserStatus
5
+ from .userauth import UserAuth