ipulse-shared-core-ftredge 19.0.1__py3-none-any.whl → 22.1.1__py3-none-any.whl
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/cache/shared_cache.py +1 -2
- ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +4 -4
- ipulse_shared_core_ftredge/exceptions/base_exceptions.py +23 -0
- ipulse_shared_core_ftredge/models/__init__.py +3 -7
- ipulse_shared_core_ftredge/models/base_data_model.py +17 -19
- ipulse_shared_core_ftredge/models/catalog/__init__.py +10 -0
- ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py +273 -0
- ipulse_shared_core_ftredge/models/catalog/usertype.py +170 -0
- ipulse_shared_core_ftredge/models/user/__init__.py +5 -0
- ipulse_shared_core_ftredge/models/user/user_permissions.py +66 -0
- ipulse_shared_core_ftredge/models/{subscription.py → user/user_subscription.py} +66 -20
- ipulse_shared_core_ftredge/models/{user_auth.py → user/userauth.py} +19 -10
- ipulse_shared_core_ftredge/models/{user_profile.py → user/userprofile.py} +53 -21
- ipulse_shared_core_ftredge/models/user/userstatus.py +430 -0
- ipulse_shared_core_ftredge/monitoring/__init__.py +2 -2
- ipulse_shared_core_ftredge/monitoring/tracemon.py +320 -0
- ipulse_shared_core_ftredge/services/__init__.py +11 -13
- ipulse_shared_core_ftredge/services/base/__init__.py +3 -1
- ipulse_shared_core_ftredge/services/base/base_firestore_service.py +73 -14
- ipulse_shared_core_ftredge/services/{cache_aware_firestore_service.py → base/cache_aware_firestore_service.py} +46 -32
- ipulse_shared_core_ftredge/services/catalog/__init__.py +14 -0
- ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py +273 -0
- ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py +307 -0
- ipulse_shared_core_ftredge/services/charging_processors.py +25 -25
- ipulse_shared_core_ftredge/services/user/__init__.py +5 -25
- ipulse_shared_core_ftredge/services/user/firebase_auth_admin_helpers.py +160 -0
- ipulse_shared_core_ftredge/services/user/user_core_service.py +423 -515
- ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +726 -0
- ipulse_shared_core_ftredge/services/user/user_permissions_operations.py +392 -0
- ipulse_shared_core_ftredge/services/user/user_subscription_operations.py +484 -0
- ipulse_shared_core_ftredge/services/user/userauth_operations.py +928 -0
- ipulse_shared_core_ftredge/services/user/userprofile_operations.py +166 -0
- ipulse_shared_core_ftredge/services/user/userstatus_operations.py +212 -0
- ipulse_shared_core_ftredge/services/{charging_service.py → user_charging_service.py} +9 -9
- {ipulse_shared_core_ftredge-19.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/METADATA +3 -4
- ipulse_shared_core_ftredge-22.1.1.dist-info/RECORD +51 -0
- ipulse_shared_core_ftredge/models/user_status.py +0 -495
- ipulse_shared_core_ftredge/monitoring/microservmon.py +0 -483
- ipulse_shared_core_ftredge/services/user/iam_management_operations.py +0 -326
- ipulse_shared_core_ftredge/services/user/subscription_management_operations.py +0 -384
- ipulse_shared_core_ftredge/services/user/user_account_operations.py +0 -479
- ipulse_shared_core_ftredge/services/user/user_auth_operations.py +0 -305
- ipulse_shared_core_ftredge/services/user/user_holistic_operations.py +0 -436
- ipulse_shared_core_ftredge-19.0.1.dist-info/RECORD +0 -41
- {ipulse_shared_core_ftredge-19.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/WHEEL +0 -0
- {ipulse_shared_core_ftredge-19.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/licenses/LICENCE +0 -0
- {ipulse_shared_core_ftredge-19.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/top_level.txt +0 -0
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
from datetime import datetime, timezone
|
|
2
2
|
from dateutil.relativedelta import relativedelta
|
|
3
3
|
import uuid
|
|
4
|
-
from typing import
|
|
5
|
-
from pydantic import Field, ConfigDict
|
|
6
|
-
from ipulse_shared_base_ftredge import Layer, Module,
|
|
7
|
-
from
|
|
8
|
-
from .
|
|
4
|
+
from typing import Optional, ClassVar, Dict, Any, List
|
|
5
|
+
from pydantic import Field, ConfigDict, model_validator
|
|
6
|
+
from ipulse_shared_base_ftredge import Layer, Module, list_enums_as_lower_strings, Subject, SubscriptionPlanName, SubscriptionStatus
|
|
7
|
+
from ..base_data_model import BaseDataModel
|
|
8
|
+
from .user_permissions import UserPermission
|
|
9
9
|
# ORIGINAL AUTHOR ="russlan.ramdowar;russlan@ftredge.com"
|
|
10
10
|
# CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
DEFAULT_SUBSCRIPTION_PLAN =
|
|
13
|
+
DEFAULT_SUBSCRIPTION_PLAN = SubscriptionPlanName.FREE_SUBSCRIPTION
|
|
14
14
|
DEFAULT_SUBSCRIPTION_STATUS = SubscriptionStatus.ACTIVE
|
|
15
15
|
|
|
16
16
|
############################################ !!!!! ALWAYS UPDATE SCHEMA VERSION , IF SCHEMA IS BEING MODIFIED !!! ############################################
|
|
17
|
-
class
|
|
17
|
+
class UserSubscription(BaseDataModel):
|
|
18
18
|
"""
|
|
19
19
|
Represents a single subscription cycle with enhanced flexibility and tracking.
|
|
20
20
|
"""
|
|
@@ -22,7 +22,7 @@ class Subscription(BaseDataModel):
|
|
|
22
22
|
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
23
23
|
|
|
24
24
|
VERSION: ClassVar[float] = 3.0 # Incremented version for direct fields instead of computed
|
|
25
|
-
DOMAIN: ClassVar[str] = "_".join(
|
|
25
|
+
DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE, Subject.SUBSCRIPTION))
|
|
26
26
|
OBJ_REF: ClassVar[str] = "subscription"
|
|
27
27
|
|
|
28
28
|
# System-managed fields (read-only)
|
|
@@ -33,13 +33,13 @@ class Subscription(BaseDataModel):
|
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
# Unique identifier for this specific subscription instance - now auto-generated
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
id: Optional[str] = Field(
|
|
37
|
+
default=None, # Will be auto-generated using UUID if not provided
|
|
38
38
|
description="Unique identifier for this subscription instance"
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
# Plan identification
|
|
42
|
-
plan_name:
|
|
42
|
+
plan_name: SubscriptionPlanName = Field(
|
|
43
43
|
..., # Required field, no default
|
|
44
44
|
description="Subscription Plan Name"
|
|
45
45
|
)
|
|
@@ -61,10 +61,10 @@ class Subscription(BaseDataModel):
|
|
|
61
61
|
description="Subscription Cycle Start Date"
|
|
62
62
|
)
|
|
63
63
|
|
|
64
|
-
# Direct field instead of computed
|
|
65
|
-
cycle_end_date: datetime = Field(
|
|
66
|
-
|
|
67
|
-
description="Subscription Cycle End Date"
|
|
64
|
+
# Direct field instead of computed - will be auto-calculated
|
|
65
|
+
cycle_end_date: Optional[datetime] = Field(
|
|
66
|
+
default=None,
|
|
67
|
+
description="Subscription Cycle End Date (auto-calculated if not provided)"
|
|
68
68
|
)
|
|
69
69
|
|
|
70
70
|
# Fields for cycle calculation
|
|
@@ -89,14 +89,14 @@ class Subscription(BaseDataModel):
|
|
|
89
89
|
description="Subscription Status (active, trial, pending_confirmation, etc.)"
|
|
90
90
|
)
|
|
91
91
|
|
|
92
|
-
# IAM permissions structure
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
description="IAM
|
|
92
|
+
# IAM permissions structure - simplified flattened list
|
|
93
|
+
granted_iam_permissions: List[UserPermission] = Field(
|
|
94
|
+
default_factory=list,
|
|
95
|
+
description="IAM permissions granted by this subscription"
|
|
96
96
|
)
|
|
97
97
|
|
|
98
98
|
fallback_plan_id: Optional[str] = Field(
|
|
99
|
-
|
|
99
|
+
default=None, # Optional field with None default
|
|
100
100
|
description="ID of the plan to fall back to if this subscription expires"
|
|
101
101
|
)
|
|
102
102
|
|
|
@@ -142,6 +142,46 @@ class Subscription(BaseDataModel):
|
|
|
142
142
|
description="Additional metadata for the subscription"
|
|
143
143
|
)
|
|
144
144
|
|
|
145
|
+
@model_validator(mode='before')
|
|
146
|
+
@classmethod
|
|
147
|
+
def ensure_id_exists(cls, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
148
|
+
"""
|
|
149
|
+
Ensures the id field exists by generating it using UUID if needed.
|
|
150
|
+
"""
|
|
151
|
+
if not isinstance(data, dict):
|
|
152
|
+
return data
|
|
153
|
+
|
|
154
|
+
# If id is already provided and non-empty, leave it alone
|
|
155
|
+
if data.get('id'):
|
|
156
|
+
return data
|
|
157
|
+
|
|
158
|
+
# Generate a UUID-based id if not provided
|
|
159
|
+
data['id'] = str(uuid.uuid4())
|
|
160
|
+
return data
|
|
161
|
+
|
|
162
|
+
@model_validator(mode='before')
|
|
163
|
+
@classmethod
|
|
164
|
+
def auto_calculate_cycle_end_date(cls, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
165
|
+
"""
|
|
166
|
+
Auto-calculate cycle_end_date if not provided, based on cycle_start_date,
|
|
167
|
+
validity_time_length, and validity_time_unit.
|
|
168
|
+
"""
|
|
169
|
+
if not isinstance(data, dict):
|
|
170
|
+
return data
|
|
171
|
+
|
|
172
|
+
# Only calculate if cycle_end_date is not already provided
|
|
173
|
+
if 'cycle_end_date' not in data or data['cycle_end_date'] is None:
|
|
174
|
+
cycle_start_date = data.get('cycle_start_date')
|
|
175
|
+
validity_time_length = data.get('validity_time_length')
|
|
176
|
+
validity_time_unit = data.get('validity_time_unit')
|
|
177
|
+
|
|
178
|
+
if cycle_start_date and validity_time_length and validity_time_unit:
|
|
179
|
+
data['cycle_end_date'] = cls.calculate_cycle_end_date(
|
|
180
|
+
cycle_start_date, validity_time_length, validity_time_unit
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return data
|
|
184
|
+
|
|
145
185
|
# Helper method to calculate cycle end date
|
|
146
186
|
@classmethod
|
|
147
187
|
def calculate_cycle_end_date(cls, start_date: datetime, validity_length: int, validity_unit: str) -> datetime:
|
|
@@ -162,6 +202,8 @@ class Subscription(BaseDataModel):
|
|
|
162
202
|
# Methods for subscription management
|
|
163
203
|
def is_active(self) -> bool:
|
|
164
204
|
"""Check if the subscription is currently active."""
|
|
205
|
+
if not self.cycle_end_date:
|
|
206
|
+
return False
|
|
165
207
|
now = datetime.now(timezone.utc)
|
|
166
208
|
return (
|
|
167
209
|
self.status == SubscriptionStatus.ACTIVE and
|
|
@@ -170,11 +212,15 @@ class Subscription(BaseDataModel):
|
|
|
170
212
|
|
|
171
213
|
def is_expired(self) -> bool:
|
|
172
214
|
"""Check if the subscription has expired."""
|
|
215
|
+
if not self.cycle_end_date:
|
|
216
|
+
return True
|
|
173
217
|
now = datetime.now(timezone.utc)
|
|
174
218
|
return now > self.cycle_end_date
|
|
175
219
|
|
|
176
220
|
def days_remaining(self) -> int:
|
|
177
221
|
"""Calculate the number of days remaining in the subscription."""
|
|
222
|
+
if not self.cycle_end_date:
|
|
223
|
+
return 0
|
|
178
224
|
now = datetime.now(timezone.utc)
|
|
179
225
|
if now > self.cycle_end_date:
|
|
180
226
|
return 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Optional, Dict, Any, List
|
|
2
2
|
from datetime import datetime
|
|
3
|
-
from pydantic import BaseModel, Field, EmailStr, ConfigDict, field_validator
|
|
3
|
+
from pydantic import BaseModel, Field, EmailStr, ConfigDict, field_validator, model_validator
|
|
4
4
|
|
|
5
5
|
class UserAuth(BaseModel):
|
|
6
6
|
"""Comprehensive authentication model for user credentials and auth operations"""
|
|
@@ -8,17 +8,18 @@ class UserAuth(BaseModel):
|
|
|
8
8
|
|
|
9
9
|
# Core authentication fields
|
|
10
10
|
email: EmailStr = Field(..., description="User's email address")
|
|
11
|
+
display_name: Optional[str] = Field(None, description="User's display name")
|
|
11
12
|
password: Optional[str] = Field(None, min_length=6, description="User's password (for creation/update only)")
|
|
12
13
|
|
|
13
14
|
# Firebase Auth specific fields
|
|
14
|
-
firebase_uid: Optional[str] = Field(None, description="Firebase Auth UID")
|
|
15
|
+
firebase_uid: Optional[str] = Field(default=None, description="Firebase Auth UID")
|
|
15
16
|
provider_id: str = Field(default="password", description="Authentication provider ID")
|
|
16
17
|
email_verified: bool = Field(default=False, description="Whether email is verified")
|
|
17
18
|
disabled: bool = Field(default=False, description="Whether user account is disabled")
|
|
18
19
|
|
|
19
20
|
# Multi-factor authentication
|
|
20
21
|
mfa_enabled: bool = Field(default=False, description="Whether MFA is enabled")
|
|
21
|
-
phone_number: Optional[str] = Field(None, description="Phone number for SMS MFA")
|
|
22
|
+
phone_number: Optional[str] = Field(default=None, description="Phone number for SMS MFA")
|
|
22
23
|
|
|
23
24
|
# Custom claims and metadata
|
|
24
25
|
custom_claims: Dict[str, Any] = Field(default_factory=dict, description="Firebase custom claims")
|
|
@@ -28,14 +29,14 @@ class UserAuth(BaseModel):
|
|
|
28
29
|
provider_data: List[Dict[str, Any]] = Field(default_factory=list, description="Provider-specific data")
|
|
29
30
|
|
|
30
31
|
# Account management
|
|
31
|
-
created_at: Optional[datetime] = Field(None, description="Account creation timestamp")
|
|
32
|
-
last_sign_in: Optional[datetime] = Field(None, description="Last sign-in timestamp")
|
|
33
|
-
last_refresh: Optional[datetime] = Field(None, description="Last token refresh timestamp")
|
|
32
|
+
created_at: Optional[datetime] = Field(default=None, description="Account creation timestamp")
|
|
33
|
+
last_sign_in: Optional[datetime] = Field(default=None, description="Last sign-in timestamp")
|
|
34
|
+
last_refresh: Optional[datetime] = Field(default=None, description="Last token refresh timestamp")
|
|
34
35
|
|
|
35
36
|
# Password management
|
|
36
|
-
password_hash: Optional[str] = Field(None, description="Password hash (internal use only)")
|
|
37
|
-
password_salt: Optional[str] = Field(None, description="Password salt (internal use only)")
|
|
38
|
-
valid_since: Optional[datetime] = Field(None, description="Timestamp since when tokens are valid")
|
|
37
|
+
password_hash: Optional[str] = Field(default=None, description="Password hash (internal use only)")
|
|
38
|
+
password_salt: Optional[str] = Field(default=None, description="Password salt (internal use only)")
|
|
39
|
+
valid_since: Optional[datetime] = Field(default=None, description="Timestamp since when tokens are valid")
|
|
39
40
|
|
|
40
41
|
@field_validator('phone_number')
|
|
41
42
|
@classmethod
|
|
@@ -61,4 +62,12 @@ class UserAuth(BaseModel):
|
|
|
61
62
|
if claim in reserved_claims:
|
|
62
63
|
raise ValueError(f'Custom claim "{claim}" is reserved by Firebase')
|
|
63
64
|
|
|
64
|
-
return v
|
|
65
|
+
return v
|
|
66
|
+
|
|
67
|
+
@model_validator(mode='before')
|
|
68
|
+
@classmethod
|
|
69
|
+
def check_password_fields(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
70
|
+
"""Ensure that password and password_hash are not set simultaneously."""
|
|
71
|
+
if values.get('password') and values.get('password_hash'):
|
|
72
|
+
raise ValueError('Cannot set both password and password_hash')
|
|
73
|
+
return values
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
""" User Profile model for storing personal information and settings. """
|
|
2
|
-
from datetime import date, datetime
|
|
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
|
|
2
|
+
from datetime import date, datetime
|
|
7
3
|
import re # Add re import
|
|
8
|
-
|
|
4
|
+
from typing import Set, Optional, ClassVar, Dict, Any, List
|
|
5
|
+
from pydantic import EmailStr, Field, ConfigDict, model_validator, field_validator
|
|
6
|
+
from ipulse_shared_base_ftredge import Layer, Module, list_enums_as_lower_strings, Subject, IAMUserType
|
|
7
|
+
from ..base_data_model import BaseDataModel
|
|
9
8
|
# ORIGINAL AUTHOR ="Russlan Ramdowar;russlan@ftredge.com"
|
|
10
9
|
# CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
|
|
11
10
|
|
|
@@ -18,7 +17,7 @@ class UserProfile(BaseDataModel):
|
|
|
18
17
|
|
|
19
18
|
# Class constants
|
|
20
19
|
VERSION: ClassVar[float] = 5.0 # Incremented version for primary_usertype addition
|
|
21
|
-
DOMAIN: ClassVar[str] = "_".join(
|
|
20
|
+
DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE, Subject.USER))
|
|
22
21
|
OBJ_REF: ClassVar[str] = "userprofile"
|
|
23
22
|
|
|
24
23
|
schema_version: float = Field(
|
|
@@ -27,26 +26,28 @@ class UserProfile(BaseDataModel):
|
|
|
27
26
|
description="Version of this Class == version of DB Schema"
|
|
28
27
|
)
|
|
29
28
|
|
|
30
|
-
id: str = Field(
|
|
31
|
-
default=
|
|
29
|
+
id: Optional[str] = Field(
|
|
30
|
+
default=None, # Will be auto-generated from user_uid if not provided
|
|
32
31
|
description=f"User Profile ID, format: {OBJ_REF}_user_uid"
|
|
33
32
|
)
|
|
34
33
|
|
|
35
34
|
user_uid: str = Field(
|
|
36
35
|
...,
|
|
37
|
-
|
|
36
|
+
min_length=1,
|
|
37
|
+
description="User UID from Firebase Auth",
|
|
38
|
+
frozen=True
|
|
38
39
|
)
|
|
39
40
|
|
|
40
41
|
# Added primary_usertype field for main role categorization
|
|
41
|
-
primary_usertype:
|
|
42
|
+
primary_usertype: IAMUserType = Field(
|
|
42
43
|
...,
|
|
43
|
-
description="Primary user type
|
|
44
|
+
description="Primary user type from IAMUserType enum"
|
|
44
45
|
)
|
|
45
46
|
|
|
46
47
|
# Renamed usertypes to secondary_usertypes
|
|
47
|
-
secondary_usertypes: List[
|
|
48
|
+
secondary_usertypes: List[IAMUserType] = Field(
|
|
48
49
|
default_factory=list,
|
|
49
|
-
description="List of secondary user types"
|
|
50
|
+
description="List of secondary user types from IAMUserType enum"
|
|
50
51
|
)
|
|
51
52
|
|
|
52
53
|
# Rest of the fields remain the same
|
|
@@ -105,24 +106,38 @@ class UserProfile(BaseDataModel):
|
|
|
105
106
|
|
|
106
107
|
# Remove audit fields as they're inherited from BaseDataModel
|
|
107
108
|
|
|
109
|
+
@field_validator('user_uid')
|
|
110
|
+
@classmethod
|
|
111
|
+
def validate_user_uid(cls, v: str) -> str:
|
|
112
|
+
"""Validate that user_uid is not empty string."""
|
|
113
|
+
if not v or not v.strip():
|
|
114
|
+
raise ValueError("user_uid cannot be empty or whitespace-only")
|
|
115
|
+
return v.strip()
|
|
116
|
+
|
|
108
117
|
@model_validator(mode='before')
|
|
109
118
|
@classmethod
|
|
110
119
|
def ensure_id_exists(cls, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
111
120
|
"""
|
|
112
|
-
Ensures the id field exists
|
|
121
|
+
Ensures the id field exists and matches expected format, or generates it from user_uid.
|
|
113
122
|
This runs BEFORE validation, guaranteeing id will be present for validators.
|
|
114
123
|
"""
|
|
115
124
|
if not isinstance(data, dict):
|
|
116
125
|
return data
|
|
117
126
|
|
|
118
|
-
|
|
119
|
-
if
|
|
120
|
-
return data
|
|
127
|
+
user_uid = data.get('user_uid')
|
|
128
|
+
if not user_uid:
|
|
129
|
+
return data # Let field validation handle missing user_uid
|
|
121
130
|
|
|
122
|
-
|
|
123
|
-
if 'user_uid' in data and data['user_uid']:
|
|
124
|
-
data['id'] = f"{cls.OBJ_REF}_{data['user_uid']}"
|
|
131
|
+
expected_id = f"{cls.OBJ_REF}_{user_uid}"
|
|
125
132
|
|
|
133
|
+
# If id is already provided, validate it matches expected format
|
|
134
|
+
if data.get('id'):
|
|
135
|
+
if data['id'] != expected_id:
|
|
136
|
+
raise ValueError(f"Invalid id format. Expected '{expected_id}', got '{data['id']}'")
|
|
137
|
+
return data
|
|
138
|
+
|
|
139
|
+
# If id is not provided, generate it from user_uid
|
|
140
|
+
data['id'] = expected_id
|
|
126
141
|
return data
|
|
127
142
|
|
|
128
143
|
@model_validator(mode='before')
|
|
@@ -156,4 +171,21 @@ class UserProfile(BaseDataModel):
|
|
|
156
171
|
# Fallback if no email or username provided
|
|
157
172
|
data['username'] = "user"
|
|
158
173
|
|
|
174
|
+
return data
|
|
175
|
+
|
|
176
|
+
@model_validator(mode='before')
|
|
177
|
+
@classmethod
|
|
178
|
+
def convert_datetime_to_date(cls, data: Any) -> Any:
|
|
179
|
+
"""
|
|
180
|
+
Convert datetime objects to date objects for date fields.
|
|
181
|
+
This handles the case where Firestore returns datetime objects
|
|
182
|
+
but the model expects date objects (e.g., dob field).
|
|
183
|
+
"""
|
|
184
|
+
if not isinstance(data, dict):
|
|
185
|
+
return data
|
|
186
|
+
|
|
187
|
+
# Handle dob field specifically
|
|
188
|
+
if 'dob' in data and isinstance(data['dob'], datetime):
|
|
189
|
+
data['dob'] = data['dob'].date()
|
|
190
|
+
|
|
159
191
|
return data
|