ipulse-shared-core-ftredge 7.2.1__tar.gz → 9.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.
- {ipulse_shared_core_ftredge-7.2.1/src/ipulse_shared_core_ftredge.egg-info → ipulse_shared_core_ftredge-9.1.1}/PKG-INFO +4 -3
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/setup.py +2 -2
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/__init__.py +1 -1
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +10 -2
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/models/__init__.py +1 -1
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/models/base_api_response.py +12 -4
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/models/base_data_model.py +6 -6
- ipulse_shared_core_ftredge-9.1.1/src/ipulse_shared_core_ftredge/models/subscription.py +190 -0
- ipulse_shared_core_ftredge-9.1.1/src/ipulse_shared_core_ftredge/models/user_profile.py +125 -0
- ipulse_shared_core_ftredge-9.1.1/src/ipulse_shared_core_ftredge/models/user_status.py +585 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/services/base_firestore_service.py +10 -10
- ipulse_shared_core_ftredge-9.1.1/src/ipulse_shared_core_ftredge/utils/custom_json_encoder.py +46 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1/src/ipulse_shared_core_ftredge.egg-info}/PKG-INFO +4 -3
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge.egg-info/SOURCES.txt +1 -1
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +1 -1
- ipulse_shared_core_ftredge-7.2.1/src/ipulse_shared_core_ftredge/models/resource_catalog_item.py +0 -115
- ipulse_shared_core_ftredge-7.2.1/src/ipulse_shared_core_ftredge/models/subscription.py +0 -60
- ipulse_shared_core_ftredge-7.2.1/src/ipulse_shared_core_ftredge/models/user_profile.py +0 -96
- ipulse_shared_core_ftredge-7.2.1/src/ipulse_shared_core_ftredge/models/user_status.py +0 -106
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/LICENCE +0 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/README.md +0 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/pyproject.toml +0 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/setup.cfg +0 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +0 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_protected_router.py +0 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/dependencies/firestore_client.py +1 -1
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/models/organization_profile.py +0 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/models/user_auth.py +0 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/models/user_profile_update.py +0 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/services/__init__.py +0 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/services/base_service_exceptions.py +0 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/services/fastapiservicemon.py +0 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/services/servicemon.py +0 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge/utils/json_encoder.py +0 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge.egg-info/dependency_links.txt +0 -0
- {ipulse_shared_core_ftredge-7.2.1 → ipulse_shared_core_ftredge-9.1.1}/src/ipulse_shared_core_ftredge.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: ipulse_shared_core_ftredge
|
|
3
|
-
Version:
|
|
3
|
+
Version: 9.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
|
|
@@ -13,10 +13,11 @@ Requires-Dist: pydantic[email]~=2.5
|
|
|
13
13
|
Requires-Dist: python-dateutil~=2.8
|
|
14
14
|
Requires-Dist: fastapi~=0.115.8
|
|
15
15
|
Requires-Dist: pytest
|
|
16
|
-
Requires-Dist: ipulse_shared_base_ftredge>=
|
|
16
|
+
Requires-Dist: ipulse_shared_base_ftredge>=6.4.1
|
|
17
17
|
Dynamic: author
|
|
18
18
|
Dynamic: classifier
|
|
19
19
|
Dynamic: home-page
|
|
20
|
+
Dynamic: license-file
|
|
20
21
|
Dynamic: requires-dist
|
|
21
22
|
Dynamic: requires-python
|
|
22
23
|
Dynamic: summary
|
|
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
|
|
|
3
3
|
|
|
4
4
|
setup(
|
|
5
5
|
name='ipulse_shared_core_ftredge',
|
|
6
|
-
version='
|
|
6
|
+
version='9.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=[
|
|
@@ -12,7 +12,7 @@ setup(
|
|
|
12
12
|
'python-dateutil~=2.8',
|
|
13
13
|
'fastapi~=0.115.8',
|
|
14
14
|
'pytest',
|
|
15
|
-
'ipulse_shared_base_ftredge>=
|
|
15
|
+
'ipulse_shared_base_ftredge>=6.4.1',
|
|
16
16
|
],
|
|
17
17
|
author='Russlan Ramdowar',
|
|
18
18
|
description='Shared Core models and Logger util for the Pulse platform project. Using AI for financial advisory and investment management.',
|
|
@@ -160,7 +160,7 @@ async def authorizeAPIRequest(
|
|
|
160
160
|
) -> Dict[str, Any]:
|
|
161
161
|
"""
|
|
162
162
|
Authorize API request based on user status and OPA policies.
|
|
163
|
-
|
|
163
|
+
Enhanced with credit check information.
|
|
164
164
|
"""
|
|
165
165
|
try:
|
|
166
166
|
# Extract fields for both PATCH and POST if not provided
|
|
@@ -196,6 +196,7 @@ async def authorizeAPIRequest(
|
|
|
196
196
|
"request_resource_fields": request_resource_fields
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
####!!!!!!!!!! OPA call
|
|
199
200
|
# Query OPA
|
|
200
201
|
opa_url = f"{os.getenv('OPA_SERVER_URL', 'http://localhost:8181')}{os.getenv('OPA_DECISION_PATH', '/v1/data/http/authz/ingress/decision')}"
|
|
201
202
|
logger.debug(f"Attempting to connect to OPA at: {opa_url}")
|
|
@@ -237,11 +238,17 @@ async def authorizeAPIRequest(
|
|
|
237
238
|
}
|
|
238
239
|
)
|
|
239
240
|
|
|
241
|
+
# Extract credit check information from the OPA response
|
|
242
|
+
credit_check = {}
|
|
243
|
+
if "credit_check" in result.get("result", {}):
|
|
244
|
+
credit_check = result["result"]["credit_check"]
|
|
245
|
+
|
|
240
246
|
# More descriptive metadata about the data freshness
|
|
241
247
|
return {
|
|
242
248
|
"used_cached_status": cache_used,
|
|
243
249
|
"required_fresh_status": force_fresh,
|
|
244
|
-
"status_retrieved_at": datetime.now(timezone.utc).isoformat()
|
|
250
|
+
"status_retrieved_at": datetime.now(timezone.utc).isoformat(),
|
|
251
|
+
"credit_check": credit_check
|
|
245
252
|
}
|
|
246
253
|
|
|
247
254
|
except (AuthorizationError, ResourceNotFoundError):
|
|
@@ -268,6 +275,7 @@ def _should_force_fresh_status(request: Request) -> bool:
|
|
|
268
275
|
credit_sensitive_patterns = [
|
|
269
276
|
'prediction',
|
|
270
277
|
'user-statuses',
|
|
278
|
+
'historic'
|
|
271
279
|
]
|
|
272
280
|
# Methods that require fresh status
|
|
273
281
|
sensitive_methods = {'post', 'patch', 'put', 'delete'}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from .user_profile import UserProfile
|
|
2
2
|
from .subscription import Subscription
|
|
3
|
-
from .user_status import UserStatus
|
|
3
|
+
from .user_status import UserStatus, IAMUnitRefAssignment
|
|
4
4
|
from .user_profile_update import UserProfileUpdate
|
|
5
5
|
from .user_auth import UserAuth
|
|
6
6
|
from .organization_profile import OrganizationProfile
|
|
@@ -3,10 +3,7 @@ import datetime as dt
|
|
|
3
3
|
import json
|
|
4
4
|
from pydantic import BaseModel, ConfigDict
|
|
5
5
|
from fastapi.responses import JSONResponse
|
|
6
|
-
from ipulse_shared_core_ftredge.utils
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
from ipulse_shared_core_ftredge.utils import CustomJSONEncoder
|
|
10
7
|
|
|
11
8
|
|
|
12
9
|
T = TypeVar('T')
|
|
@@ -30,6 +27,17 @@ class PaginatedAPIResponse(BaseAPIResponse, Generic[T]):
|
|
|
30
27
|
|
|
31
28
|
class CustomJSONResponse(JSONResponse):
|
|
32
29
|
def render(self, content) -> bytes:
|
|
30
|
+
# Handle Pydantic models to exclude computed fields
|
|
31
|
+
if isinstance(content, dict) and "data" in content and hasattr(content["data"], "model_dump"):
|
|
32
|
+
# If content["data"] is a Pydantic model, use model_dump with exclude_unset=True
|
|
33
|
+
# and exclude_computed=True to prevent serialization of computed fields
|
|
34
|
+
content = dict(content) # Create a copy to avoid modifying the original
|
|
35
|
+
content["data"] = content["data"].model_dump(
|
|
36
|
+
exclude_unset=True,
|
|
37
|
+
exclude_computed=True
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Use the CustomJSONEncoder for serialization
|
|
33
41
|
return json.dumps(
|
|
34
42
|
content,
|
|
35
43
|
ensure_ascii=False,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from datetime import datetime
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
2
|
from typing import ClassVar
|
|
3
3
|
from pydantic import BaseModel, Field, ConfigDict, field_validator
|
|
4
4
|
import dateutil.parser
|
|
@@ -20,17 +20,17 @@ class BaseDataModel(BaseModel):
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
# Audit fields
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), frozen=True)
|
|
24
|
+
created_by: str = Field(..., frozen=True)
|
|
25
|
+
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), frozen=True)
|
|
26
|
+
updated_by: str = Field(...)
|
|
27
27
|
|
|
28
28
|
@classmethod
|
|
29
29
|
def get_collection_name(cls) -> str:
|
|
30
30
|
"""Generate standard collection name"""
|
|
31
31
|
return f"{cls.DOMAIN}_{cls.OBJ_REF}s"
|
|
32
32
|
|
|
33
|
-
@field_validator('
|
|
33
|
+
@field_validator('created_at', 'updated_at', mode='before')
|
|
34
34
|
@classmethod
|
|
35
35
|
def parse_datetime(cls, v: any) -> datetime:
|
|
36
36
|
if isinstance(v, datetime):
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from dateutil.relativedelta import relativedelta
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Set, Optional, ClassVar, Dict, Any, List, Union
|
|
5
|
+
from pydantic import Field, ConfigDict
|
|
6
|
+
from ipulse_shared_base_ftredge import Layer, Module, list_as_lower_strings, Subject, SubscriptionPlan, SubscriptionStatus
|
|
7
|
+
from ipulse_shared_base_ftredge.enums.enums_iam import IAMUnitType
|
|
8
|
+
from .base_data_model import BaseDataModel
|
|
9
|
+
# ORIGINAL AUTHOR ="russlan.ramdowar;russlan@ftredge.com"
|
|
10
|
+
# CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DEFAULT_SUBSCRIPTION_PLAN = SubscriptionPlan.FREE
|
|
14
|
+
DEFAULT_SUBSCRIPTION_STATUS = SubscriptionStatus.ACTIVE
|
|
15
|
+
|
|
16
|
+
############################################ !!!!! ALWAYS UPDATE SCHEMA VERSION , IF SCHEMA IS BEING MODIFIED !!! ############################################
|
|
17
|
+
class Subscription(BaseDataModel):
|
|
18
|
+
"""
|
|
19
|
+
Represents a single subscription cycle with enhanced flexibility and tracking.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
23
|
+
|
|
24
|
+
VERSION: ClassVar[float] = 3.0 # Incremented version for direct fields instead of computed
|
|
25
|
+
DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.SUBSCRIPTION.name))
|
|
26
|
+
OBJ_REF: ClassVar[str] = "subscription"
|
|
27
|
+
|
|
28
|
+
# System-managed fields (read-only)
|
|
29
|
+
schema_version: float = Field(
|
|
30
|
+
default=VERSION,
|
|
31
|
+
description="Version of this Class == version of DB Schema",
|
|
32
|
+
frozen=True
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Unique identifier for this specific subscription instance - now auto-generated
|
|
36
|
+
uuid: str = Field(
|
|
37
|
+
default_factory=lambda: str(uuid.uuid4()),
|
|
38
|
+
description="Unique identifier for this subscription instance"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Plan identification
|
|
42
|
+
plan_name: SubscriptionPlan = Field(
|
|
43
|
+
..., # Required field, no default
|
|
44
|
+
description="Subscription Plan Name"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
plan_version: int = Field(
|
|
48
|
+
..., # Required field, no default
|
|
49
|
+
description="Version of the subscription plan"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Direct field instead of computed
|
|
53
|
+
plan_id: str = Field(
|
|
54
|
+
..., # Required field, no default
|
|
55
|
+
description="Combined plan identifier (plan_name_plan_version)"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Cycle duration fields
|
|
59
|
+
cycle_start_date: datetime = Field(
|
|
60
|
+
..., # Required field, no default
|
|
61
|
+
description="Subscription Cycle Start Date"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Direct field instead of computed
|
|
65
|
+
cycle_end_date: datetime = Field(
|
|
66
|
+
..., # Required field, no default
|
|
67
|
+
description="Subscription Cycle End Date"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Fields for cycle calculation
|
|
71
|
+
validity_time_length: int = Field(
|
|
72
|
+
..., # Required field, no default
|
|
73
|
+
description="Length of subscription validity period (e.g., 1, 3, 12)"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
validity_time_unit: str = Field(
|
|
77
|
+
..., # Required field, no default
|
|
78
|
+
description="Unit of subscription validity ('minute', 'hour', 'day', 'week', 'month', 'year')"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Renewal and status fields
|
|
82
|
+
auto_renew: bool = Field(
|
|
83
|
+
..., # Required field, no default
|
|
84
|
+
description="Auto-renewal status"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
status: SubscriptionStatus = Field(
|
|
88
|
+
..., # Required field, no default
|
|
89
|
+
description="Subscription Status (active, trial, pending_confirmation, etc.)"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# IAM permissions structure
|
|
93
|
+
iam_domain_permissions: Dict[str, Dict[str, List[str]]] = Field(
|
|
94
|
+
..., # Required field, no default
|
|
95
|
+
description="IAM domain permissions granted by this subscription (domain -> IAM unit type -> list of unit references)"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
fallback_plan_id: Optional[str] = Field(
|
|
99
|
+
..., # Required field (can be None), no default
|
|
100
|
+
description="ID of the plan to fall back to if this subscription expires"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
price_paid_usd: float = Field(
|
|
104
|
+
..., # Required field, no default
|
|
105
|
+
description="Amount paid for this subscription in USD"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
payment_ref: Optional[str] = Field(
|
|
109
|
+
default=None,
|
|
110
|
+
description="Reference to payment transaction"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Credit management fields
|
|
114
|
+
subscription_based_insight_credits_per_update: int = Field(
|
|
115
|
+
default=0,
|
|
116
|
+
description="Number of insight credits to add on each update"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
subscription_based_insight_credits_update_freq_h: int = Field(
|
|
120
|
+
default=24,
|
|
121
|
+
description="Frequency of insight credits update in hours"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
extra_insight_credits_per_cycle: int = Field(
|
|
125
|
+
default=0,
|
|
126
|
+
description="Additional insight credits granted per subscription cycle"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
voting_credits_per_update: int = Field(
|
|
130
|
+
default=0,
|
|
131
|
+
description="Number of voting credits to add on each update"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
voting_credits_update_freq_h: int = Field(
|
|
135
|
+
default=62,
|
|
136
|
+
description="Frequency of voting credits update in hours"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# General metadata for extensibility
|
|
140
|
+
metadata: Dict[str, Any] = Field(
|
|
141
|
+
default_factory=dict,
|
|
142
|
+
description="Additional metadata for the subscription"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Helper method to calculate cycle end date
|
|
146
|
+
@classmethod
|
|
147
|
+
def calculate_cycle_end_date(cls, start_date: datetime, validity_length: int, validity_unit: str) -> datetime:
|
|
148
|
+
"""Calculate the end date based on start date and validity period."""
|
|
149
|
+
if validity_unit == "minute":
|
|
150
|
+
return start_date + relativedelta(minutes=validity_length)
|
|
151
|
+
elif validity_unit == "hour":
|
|
152
|
+
return start_date + relativedelta(hours=validity_length)
|
|
153
|
+
elif validity_unit == "day":
|
|
154
|
+
return start_date + relativedelta(days=validity_length)
|
|
155
|
+
elif validity_unit == "week":
|
|
156
|
+
return start_date + relativedelta(weeks=validity_length)
|
|
157
|
+
elif validity_unit == "year":
|
|
158
|
+
return start_date + relativedelta(years=validity_length)
|
|
159
|
+
else: # Default to months
|
|
160
|
+
return start_date + relativedelta(months=validity_length)
|
|
161
|
+
|
|
162
|
+
# Methods for subscription management
|
|
163
|
+
def is_active(self) -> bool:
|
|
164
|
+
"""Check if the subscription is currently active."""
|
|
165
|
+
now = datetime.now(timezone.utc)
|
|
166
|
+
return (
|
|
167
|
+
self.status == SubscriptionStatus.ACTIVE and
|
|
168
|
+
self.cycle_start_date <= now <= self.cycle_end_date
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def is_expired(self) -> bool:
|
|
172
|
+
"""Check if the subscription has expired."""
|
|
173
|
+
now = datetime.now(timezone.utc)
|
|
174
|
+
return now > self.cycle_end_date
|
|
175
|
+
|
|
176
|
+
def days_remaining(self) -> int:
|
|
177
|
+
"""Calculate the number of days remaining in the subscription."""
|
|
178
|
+
now = datetime.now(timezone.utc)
|
|
179
|
+
if now > self.cycle_end_date:
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
# Get time difference
|
|
183
|
+
time_diff = self.cycle_end_date - now
|
|
184
|
+
|
|
185
|
+
# If there's any time remaining but less than a day, return 1
|
|
186
|
+
if time_diff.days == 0 and time_diff.seconds > 0:
|
|
187
|
+
return 1
|
|
188
|
+
|
|
189
|
+
# Otherwise return the number of complete days
|
|
190
|
+
return time_diff.days
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
""" User Profile model for storing personal information and settings. """
|
|
2
|
+
from datetime import date, datetime, timezone
|
|
3
|
+
from typing import Set, Optional, ClassVar, Dict, Any, List
|
|
4
|
+
from pydantic import EmailStr, Field, ConfigDict, field_validator, model_validator, computed_field
|
|
5
|
+
from ipulse_shared_base_ftredge import Layer, Module, list_as_lower_strings, Subject
|
|
6
|
+
from .base_data_model import BaseDataModel
|
|
7
|
+
|
|
8
|
+
# ORIGINAL AUTHOR ="Russlan Ramdowar;russlan@ftredge.com"
|
|
9
|
+
# CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
|
|
10
|
+
|
|
11
|
+
############################ !!!!! ALWAYS UPDATE SCHEMA VERSION , IF SCHEMA IS BEING MODIFIED !!! #################################
|
|
12
|
+
class UserProfile(BaseDataModel):
|
|
13
|
+
"""
|
|
14
|
+
User Profile model for storing personal information and settings.
|
|
15
|
+
"""
|
|
16
|
+
model_config = ConfigDict(frozen=False, extra="forbid") # Allow field modification
|
|
17
|
+
|
|
18
|
+
# Class constants
|
|
19
|
+
VERSION: ClassVar[float] = 5.0 # Incremented version for primary_user_type addition
|
|
20
|
+
DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.USER.name))
|
|
21
|
+
OBJ_REF: ClassVar[str] = "userprofile"
|
|
22
|
+
|
|
23
|
+
schema_version: float = Field(
|
|
24
|
+
default=VERSION,
|
|
25
|
+
frozen=True,
|
|
26
|
+
description="Version of this Class == version of DB Schema"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
id: str = Field(
|
|
30
|
+
..., # Required, but will be auto-generated if not provided
|
|
31
|
+
description=f"User Profile ID, format: {OBJ_REF}_user_uid"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
user_uid: str = Field(
|
|
35
|
+
...,
|
|
36
|
+
description="User UID from Firebase Auth"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Added primary_user_type field for main role categorization
|
|
40
|
+
primary_user_type: str = Field(
|
|
41
|
+
...,
|
|
42
|
+
description="Primary user type (e.g., customer, internal, admin, superadmin)"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Renamed user_types to secondary_user_types
|
|
46
|
+
secondary_user_types: List[str] = Field(
|
|
47
|
+
default_factory=list,
|
|
48
|
+
description="List of secondary user types"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Rest of the fields remain the same
|
|
52
|
+
email: EmailStr = Field(
|
|
53
|
+
...,
|
|
54
|
+
description="Email address",
|
|
55
|
+
frozen=True
|
|
56
|
+
)
|
|
57
|
+
organizations_uids: Set[str] = Field(
|
|
58
|
+
default_factory=set,
|
|
59
|
+
description="Organization UIDs the user belongs to"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# System identification (read-only)
|
|
63
|
+
provider_id: str = Field(
|
|
64
|
+
...,
|
|
65
|
+
description="User provider ID",
|
|
66
|
+
frozen=True
|
|
67
|
+
)
|
|
68
|
+
aliases: Optional[Dict[str, str]] = Field(
|
|
69
|
+
default=None,
|
|
70
|
+
description="User aliases. With alias as key and description as value."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# User-editable fields
|
|
74
|
+
username: Optional[str] = Field(
|
|
75
|
+
default=None,
|
|
76
|
+
max_length=50,
|
|
77
|
+
pattern="^[a-zA-Z0-9_-]+$",
|
|
78
|
+
description="Username (public display name)"
|
|
79
|
+
)
|
|
80
|
+
dob: Optional[date] = Field(
|
|
81
|
+
default=None,
|
|
82
|
+
description="Date of birth"
|
|
83
|
+
)
|
|
84
|
+
first_name: Optional[str] = Field(
|
|
85
|
+
default=None,
|
|
86
|
+
max_length=100,
|
|
87
|
+
description="First name"
|
|
88
|
+
)
|
|
89
|
+
last_name: Optional[str] = Field(
|
|
90
|
+
default=None,
|
|
91
|
+
max_length=100,
|
|
92
|
+
description="Last name"
|
|
93
|
+
)
|
|
94
|
+
mobile: Optional[str] = Field(
|
|
95
|
+
default=None,
|
|
96
|
+
pattern=r"^\+?[1-9]\d{1,14}$", # Added 'r' prefix for raw string
|
|
97
|
+
description="Mobile phone number"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
metadata: Dict[str, Any] = Field(
|
|
101
|
+
default_factory=dict,
|
|
102
|
+
description="Additional metadata for the user"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Remove audit fields as they're inherited from BaseDataModel
|
|
106
|
+
|
|
107
|
+
@model_validator(mode='before')
|
|
108
|
+
@classmethod
|
|
109
|
+
def ensure_id_exists(cls, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
110
|
+
"""
|
|
111
|
+
Ensures the id field exists by generating it from user_uid if needed.
|
|
112
|
+
This runs BEFORE validation, guaranteeing id will be present for validators.
|
|
113
|
+
"""
|
|
114
|
+
if not isinstance(data, dict):
|
|
115
|
+
return data
|
|
116
|
+
|
|
117
|
+
# If id is already in the data, leave it alone
|
|
118
|
+
if 'id' in data and data['id']:
|
|
119
|
+
return data
|
|
120
|
+
|
|
121
|
+
# If user_uid exists but id doesn't, generate id from user_uid
|
|
122
|
+
if 'user_uid' in data and data['user_uid']:
|
|
123
|
+
data['id'] = f"{cls.OBJ_REF}_{data['user_uid']}"
|
|
124
|
+
|
|
125
|
+
return data
|