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,11 +1,11 @@
|
|
|
1
1
|
"""Cache-aware Firestore service base class."""
|
|
2
2
|
import time
|
|
3
|
-
from typing import TypeVar, Generic, Dict, Any, List, Optional
|
|
3
|
+
from typing import TypeVar, Generic, Dict, Any, List, Optional, Union, Type
|
|
4
4
|
from google.cloud import firestore
|
|
5
|
-
from .
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
from
|
|
5
|
+
from . import BaseFirestoreService
|
|
6
|
+
from ...exceptions import ResourceNotFoundError, ServiceError
|
|
7
|
+
from ...cache.shared_cache import SharedCache
|
|
8
|
+
from ...models import BaseDataModel
|
|
9
9
|
|
|
10
10
|
T = TypeVar('T', bound=BaseDataModel)
|
|
11
11
|
|
|
@@ -20,12 +20,13 @@ class CacheAwareFirestoreService(BaseFirestoreService[T], Generic[T]):
|
|
|
20
20
|
db: firestore.Client,
|
|
21
21
|
collection_name: str,
|
|
22
22
|
resource_type: str,
|
|
23
|
-
|
|
23
|
+
model_class: Optional[Type[T]] = None,
|
|
24
|
+
logger=None,
|
|
24
25
|
document_cache: Optional[SharedCache] = None,
|
|
25
26
|
collection_cache: Optional[SharedCache] = None,
|
|
26
27
|
timeout: float = 30.0
|
|
27
28
|
):
|
|
28
|
-
super().__init__(db, collection_name, resource_type, logger)
|
|
29
|
+
super().__init__(db, collection_name, resource_type, model_class, logger, timeout)
|
|
29
30
|
self.document_cache = document_cache
|
|
30
31
|
self.collection_cache = collection_cache
|
|
31
32
|
self.timeout = timeout
|
|
@@ -36,15 +37,16 @@ class CacheAwareFirestoreService(BaseFirestoreService[T], Generic[T]):
|
|
|
36
37
|
if self.collection_cache:
|
|
37
38
|
self.logger.info(f"Collection cache enabled for {resource_type}: {self.collection_cache.name}")
|
|
38
39
|
|
|
39
|
-
async def get_document(self, doc_id: str) -> Dict[str, Any]:
|
|
40
|
+
async def get_document(self, doc_id: str, convert_to_model: bool = True) -> Union[T, Dict[str, Any]]:
|
|
40
41
|
"""
|
|
41
42
|
Get a document with caching support.
|
|
42
43
|
|
|
43
44
|
Args:
|
|
44
45
|
doc_id: Document ID to fetch
|
|
46
|
+
convert_to_model: Whether to convert to Pydantic model
|
|
45
47
|
|
|
46
48
|
Returns:
|
|
47
|
-
Document
|
|
49
|
+
Document as model instance or dictionary
|
|
48
50
|
|
|
49
51
|
Raises:
|
|
50
52
|
ResourceNotFoundError: If document doesn't exist
|
|
@@ -57,35 +59,29 @@ class CacheAwareFirestoreService(BaseFirestoreService[T], Generic[T]):
|
|
|
57
59
|
|
|
58
60
|
if cached_doc is not None:
|
|
59
61
|
self.logger.debug(f"Cache HIT for document {doc_id} in {cache_check_time:.2f}ms")
|
|
60
|
-
|
|
62
|
+
if convert_to_model and self.model_class:
|
|
63
|
+
return self._convert_to_model(cached_doc, doc_id)
|
|
64
|
+
else:
|
|
65
|
+
cached_doc['id'] = doc_id
|
|
66
|
+
return cached_doc
|
|
61
67
|
else:
|
|
62
68
|
self.logger.debug(f"Cache MISS for document {doc_id} - checking Firestore")
|
|
63
69
|
|
|
64
|
-
# Fetch from Firestore
|
|
65
|
-
|
|
66
|
-
doc_ref = self.db.collection(self.collection_name).document(doc_id)
|
|
67
|
-
doc = doc_ref.get(timeout=self.timeout)
|
|
68
|
-
firestore_time = (time.time() - start_time) * 1000
|
|
69
|
-
|
|
70
|
-
if not doc.exists:
|
|
71
|
-
self.logger.info(f"Document {doc_id} not found in Firestore after {firestore_time:.2f}ms")
|
|
72
|
-
raise ResourceNotFoundError(self.resource_type, doc_id)
|
|
73
|
-
|
|
74
|
-
doc_data = doc.to_dict()
|
|
75
|
-
self.logger.debug(f"Fetched document {doc_id} from Firestore in {firestore_time:.2f}ms")
|
|
76
|
-
|
|
77
|
-
# Cache the result
|
|
78
|
-
if self.document_cache and doc_data:
|
|
79
|
-
self.document_cache.set(doc_id, doc_data)
|
|
80
|
-
self.logger.debug(f"Cached document {doc_id}")
|
|
70
|
+
# Fetch from Firestore using parent method
|
|
71
|
+
return await super().get_document(doc_id, convert_to_model)
|
|
81
72
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
async def get_all_documents(self, cache_key: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
73
|
+
async def get_all_documents(self, cache_key: Optional[str] = None, as_models: bool = True) -> Union[List[T], List[Dict[str, Any]]]:
|
|
85
74
|
"""
|
|
86
75
|
Retrieves all documents from the collection.
|
|
87
76
|
Uses collection_cache if cache_key is provided and cache is available.
|
|
88
77
|
Also populates document_cache for each retrieved document.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
cache_key: Optional cache key for collection-level caching
|
|
81
|
+
as_models: Whether to convert documents to Pydantic models
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
List of documents as model instances or dicts
|
|
89
85
|
"""
|
|
90
86
|
if cache_key and self.collection_cache:
|
|
91
87
|
cached_collection_data = self.collection_cache.get(cache_key)
|
|
@@ -96,6 +92,15 @@ class CacheAwareFirestoreService(BaseFirestoreService[T], Generic[T]):
|
|
|
96
92
|
for doc_data in cached_collection_data:
|
|
97
93
|
if "id" in doc_data and not self.document_cache.get(doc_data["id"]):
|
|
98
94
|
self._cache_document_data(doc_data["id"], doc_data)
|
|
95
|
+
|
|
96
|
+
# Convert to models if requested
|
|
97
|
+
if as_models and self.model_class:
|
|
98
|
+
results = []
|
|
99
|
+
for doc_data in cached_collection_data:
|
|
100
|
+
if "id" in doc_data:
|
|
101
|
+
model_instance = self._convert_to_model(doc_data, doc_data["id"])
|
|
102
|
+
results.append(model_instance)
|
|
103
|
+
return results
|
|
99
104
|
return cached_collection_data
|
|
100
105
|
else:
|
|
101
106
|
self.logger.debug(f"Cache MISS for collection key '{cache_key}' in {self.collection_cache.name} - checking Firestore")
|
|
@@ -127,6 +132,15 @@ class CacheAwareFirestoreService(BaseFirestoreService[T], Generic[T]):
|
|
|
127
132
|
# _cache_document_data expects 'id' to be in doc_data for keying
|
|
128
133
|
self._cache_document_data(doc_data["id"], doc_data)
|
|
129
134
|
|
|
135
|
+
# Convert to models if requested
|
|
136
|
+
if as_models and self.model_class:
|
|
137
|
+
results = []
|
|
138
|
+
for doc_data in docs_data_list:
|
|
139
|
+
if "id" in doc_data:
|
|
140
|
+
model_instance = self._convert_to_model(doc_data, doc_data["id"])
|
|
141
|
+
results.append(model_instance)
|
|
142
|
+
return results
|
|
143
|
+
|
|
130
144
|
return docs_data_list
|
|
131
145
|
|
|
132
146
|
except Exception as e:
|
|
@@ -139,9 +153,9 @@ class CacheAwareFirestoreService(BaseFirestoreService[T], Generic[T]):
|
|
|
139
153
|
self.document_cache.set(doc_id, data)
|
|
140
154
|
self.logger.debug(f"Cached item {doc_id} in {self.document_cache.name}")
|
|
141
155
|
|
|
142
|
-
async def create_document(self, doc_id: str, data: T, creator_uid: str) -> Dict[str, Any]:
|
|
156
|
+
async def create_document(self, doc_id: str, data: Union[T, Dict[str, Any]], creator_uid: str, merge: bool = False) -> Dict[str, Any]:
|
|
143
157
|
"""Create document and invalidate cache."""
|
|
144
|
-
result = await super().create_document(doc_id, data, creator_uid)
|
|
158
|
+
result = await super().create_document(doc_id, data, creator_uid, merge)
|
|
145
159
|
self._invalidate_document_cache(doc_id)
|
|
146
160
|
self._invalidate_all_collection_caches()
|
|
147
161
|
return result
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Catalog Services Module
|
|
3
|
+
|
|
4
|
+
This module provides services for managing catalog data including subscription plans
|
|
5
|
+
and user type templates stored in Firestore.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .catalog_subscriptionplan_service import CatalogSubscriptionPlanService
|
|
9
|
+
from .catalog_usertype_service import CatalogUserTypeService
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"CatalogSubscriptionPlanService",
|
|
13
|
+
"CatalogUserTypeService",
|
|
14
|
+
]
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Subscription Plan Catalog Service
|
|
3
|
+
|
|
4
|
+
This service manages subscription plan templates stored in Firestore.
|
|
5
|
+
These templates are used to configure and create user subscriptions consistently.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Dict, List, Optional, Any
|
|
10
|
+
from google.cloud import firestore
|
|
11
|
+
from google.cloud.firestore import Client
|
|
12
|
+
from ipulse_shared_base_ftredge import SubscriptionPlanName
|
|
13
|
+
from ipulse_shared_base_ftredge.enums.enums_status import ObjectOverallStatus
|
|
14
|
+
from ipulse_shared_core_ftredge.models.catalog.subscriptionplan import SubscriptionPlan
|
|
15
|
+
from ipulse_shared_core_ftredge.services.base.base_firestore_service import BaseFirestoreService
|
|
16
|
+
from ipulse_shared_core_ftredge.exceptions import ServiceError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CatalogSubscriptionPlanService(BaseFirestoreService[SubscriptionPlan]):
|
|
20
|
+
"""
|
|
21
|
+
Service for managing subscription plan catalog configurations.
|
|
22
|
+
|
|
23
|
+
This service provides CRUD operations for subscription plan templates that define
|
|
24
|
+
the structure and defaults for user subscriptions.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
firestore_client: Client,
|
|
30
|
+
logger: Optional[logging.Logger] = None
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Initialize the Subscription Plan Catalog Service.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
firestore_client: Firestore client instance
|
|
37
|
+
logger: Logger instance (optional)
|
|
38
|
+
"""
|
|
39
|
+
super().__init__(
|
|
40
|
+
db=firestore_client,
|
|
41
|
+
collection_name="papp_core_catalog_subscriptionplans",
|
|
42
|
+
resource_type="subscriptionplan",
|
|
43
|
+
model_class=SubscriptionPlan,
|
|
44
|
+
logger=logger or logging.getLogger(__name__)
|
|
45
|
+
)
|
|
46
|
+
self.archive_collection_name = "~archive_papp_core_catalog_subscriptionplans"
|
|
47
|
+
|
|
48
|
+
async def create_subscriptionplan(
|
|
49
|
+
self,
|
|
50
|
+
subscriptionplan_id: str,
|
|
51
|
+
subscription_plan: SubscriptionPlan,
|
|
52
|
+
creator_uid: str
|
|
53
|
+
) -> SubscriptionPlan:
|
|
54
|
+
"""
|
|
55
|
+
Create a new subscription plan.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
subscriptionplan_id: Unique identifier for the plan
|
|
59
|
+
subscription_plan: Subscription plan data
|
|
60
|
+
creator_uid: UID of the user creating the plan
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Created subscription plan
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
ServiceError: If creation fails
|
|
67
|
+
ValidationError: If plan data is invalid
|
|
68
|
+
"""
|
|
69
|
+
self.logger.info(f"Creating subscription plan: {subscriptionplan_id}")
|
|
70
|
+
|
|
71
|
+
# Create the document
|
|
72
|
+
created_doc = await self.create_document(
|
|
73
|
+
doc_id=subscriptionplan_id,
|
|
74
|
+
data=subscription_plan,
|
|
75
|
+
creator_uid=creator_uid
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Convert back to model
|
|
79
|
+
result = SubscriptionPlan.model_validate(created_doc)
|
|
80
|
+
self.logger.info(f"Successfully created subscription plan: {subscriptionplan_id}")
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
async def get_subscriptionplan(self, subscriptionplan_id: str) -> Optional[SubscriptionPlan]:
|
|
84
|
+
"""
|
|
85
|
+
Retrieve a subscription plan by ID.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
subscriptionplan_id: Unique identifier for the plan
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Subscription plan if found, None otherwise
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ServiceError: If retrieval fails
|
|
95
|
+
"""
|
|
96
|
+
self.logger.debug(f"Retrieving subscription plan: {subscriptionplan_id}")
|
|
97
|
+
doc_data = await self.get_document(subscriptionplan_id)
|
|
98
|
+
if doc_data is None:
|
|
99
|
+
return None
|
|
100
|
+
return SubscriptionPlan.model_validate(doc_data) if isinstance(doc_data, dict) else doc_data
|
|
101
|
+
|
|
102
|
+
async def update_subscriptionplan(
|
|
103
|
+
self,
|
|
104
|
+
subscriptionplan_id: str,
|
|
105
|
+
updates: Dict[str, Any],
|
|
106
|
+
updater_uid: str
|
|
107
|
+
) -> SubscriptionPlan:
|
|
108
|
+
"""
|
|
109
|
+
Update a subscription plan.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
subscriptionplan_id: Unique identifier for the plan
|
|
113
|
+
updates: Fields to update
|
|
114
|
+
updater_uid: UID of the user updating the plan
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Updated subscription plan
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ServiceError: If update fails
|
|
121
|
+
ResourceNotFoundError: If plan not found
|
|
122
|
+
ValidationError: If update data is invalid
|
|
123
|
+
"""
|
|
124
|
+
self.logger.info(f"Updating subscription plan: {subscriptionplan_id}")
|
|
125
|
+
|
|
126
|
+
updated_doc = await self.update_document(
|
|
127
|
+
doc_id=subscriptionplan_id,
|
|
128
|
+
update_data=updates,
|
|
129
|
+
updater_uid=updater_uid
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
result = SubscriptionPlan.model_validate(updated_doc)
|
|
133
|
+
self.logger.info(f"Successfully updated subscription plan: {subscriptionplan_id}")
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
async def delete_subscriptionplan(
|
|
137
|
+
self,
|
|
138
|
+
subscriptionplan_id: str,
|
|
139
|
+
archive: bool = True
|
|
140
|
+
) -> bool:
|
|
141
|
+
"""
|
|
142
|
+
Delete a subscription plan.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
subscriptionplan_id: Unique identifier for the plan
|
|
146
|
+
archive: Whether to archive the plan before deletion
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
True if deletion was successful
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
ServiceError: If deletion fails
|
|
153
|
+
ResourceNotFoundError: If plan not found
|
|
154
|
+
"""
|
|
155
|
+
self.logger.info(f"Deleting subscription plan: {subscriptionplan_id}")
|
|
156
|
+
|
|
157
|
+
if archive:
|
|
158
|
+
# Get the plan data before deletion for archiving
|
|
159
|
+
template = await self.get_subscriptionplan(subscriptionplan_id)
|
|
160
|
+
if template:
|
|
161
|
+
await self.archive_document(
|
|
162
|
+
document_data=template.model_dump(),
|
|
163
|
+
doc_id=subscriptionplan_id,
|
|
164
|
+
archive_collection=self.archive_collection_name,
|
|
165
|
+
archived_by="system"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
result = await self.delete_document(subscriptionplan_id)
|
|
169
|
+
self.logger.info(f"Successfully deleted subscription plan: {subscriptionplan_id}")
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
async def list_subscriptionplans(
|
|
173
|
+
self,
|
|
174
|
+
plan_name: Optional[SubscriptionPlanName] = None,
|
|
175
|
+
pulse_status: Optional[ObjectOverallStatus] = None,
|
|
176
|
+
latest_version_only: bool = False,
|
|
177
|
+
limit: Optional[int] = None
|
|
178
|
+
) -> List[SubscriptionPlan]:
|
|
179
|
+
"""
|
|
180
|
+
List subscription plans with optional filtering.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
plan_name: Filter by specific plan name (FREE, BASE, PREMIUM)
|
|
184
|
+
pulse_status: Filter by specific pulse status
|
|
185
|
+
latest_version_only: Only return the latest version per plan
|
|
186
|
+
limit: Maximum number of plans to return
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
List of subscription plans
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
ServiceError: If listing fails
|
|
193
|
+
"""
|
|
194
|
+
self.logger.debug(f"Listing subscription plans - plan_name: {plan_name}, pulse_status: {pulse_status}, latest_version_only: {latest_version_only}")
|
|
195
|
+
|
|
196
|
+
# Build query filters
|
|
197
|
+
filters = []
|
|
198
|
+
if plan_name:
|
|
199
|
+
filters.append(("plan_name", "==", plan_name.value))
|
|
200
|
+
if pulse_status:
|
|
201
|
+
filters.append(("pulse_status", "==", pulse_status.value))
|
|
202
|
+
|
|
203
|
+
# If latest_version_only is requested, order by version descending
|
|
204
|
+
order_by = None
|
|
205
|
+
if latest_version_only:
|
|
206
|
+
order_by = "plan_version" # Use plan_version field name
|
|
207
|
+
|
|
208
|
+
docs = await self.list_documents(
|
|
209
|
+
filters=filters,
|
|
210
|
+
order_by=order_by,
|
|
211
|
+
limit=limit
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Convert to SubscriptionPlan models
|
|
215
|
+
plans = [SubscriptionPlan.model_validate(doc) if isinstance(doc, dict) else doc for doc in docs]
|
|
216
|
+
|
|
217
|
+
# If latest_version_only is requested, group by plan_name and get highest version
|
|
218
|
+
if latest_version_only:
|
|
219
|
+
# Group by plan_name
|
|
220
|
+
plan_groups = {}
|
|
221
|
+
for plan in plans:
|
|
222
|
+
key = plan.plan_name.value
|
|
223
|
+
if key not in plan_groups:
|
|
224
|
+
plan_groups[key] = []
|
|
225
|
+
plan_groups[key].append(plan)
|
|
226
|
+
|
|
227
|
+
# Get the latest version for each group
|
|
228
|
+
latest_plans = []
|
|
229
|
+
for group in plan_groups.values():
|
|
230
|
+
if group:
|
|
231
|
+
# Sort by plan_version descending and take the first
|
|
232
|
+
latest = max(group, key=lambda x: x.plan_version)
|
|
233
|
+
latest_plans.append(latest)
|
|
234
|
+
|
|
235
|
+
return latest_plans
|
|
236
|
+
|
|
237
|
+
return plans
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _get_collection(self):
|
|
241
|
+
"""Get the Firestore collection reference."""
|
|
242
|
+
return self.db.collection(self.collection_name)
|
|
243
|
+
|
|
244
|
+
async def subscriptionplan_exists(self, subscriptionplan_id: str) -> bool:
|
|
245
|
+
"""
|
|
246
|
+
Check if a subscription plan exists.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
subscriptionplan_id: Unique identifier for the plan
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
True if plan exists, False otherwise
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
ServiceError: If check fails
|
|
256
|
+
"""
|
|
257
|
+
return await self.document_exists(subscriptionplan_id)
|
|
258
|
+
|
|
259
|
+
async def validate_subscriptionplan_data(self, subscriptionplan_data: Dict[str, Any]) -> tuple[bool, List[str]]:
|
|
260
|
+
"""
|
|
261
|
+
Validate subscription plan data.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
subscriptionplan_data: Plan data to validate
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Tuple of (is_valid, list_of_errors)
|
|
268
|
+
"""
|
|
269
|
+
try:
|
|
270
|
+
SubscriptionPlan.model_validate(subscriptionplan_data)
|
|
271
|
+
return True, []
|
|
272
|
+
except Exception as e:
|
|
273
|
+
return False, [str(e)]
|