ipulse-shared-core-ftredge 15.0.1__tar.gz → 18.0.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 (41) hide show
  1. {ipulse_shared_core_ftredge-15.0.1/src/ipulse_shared_core_ftredge.egg-info → ipulse_shared_core_ftredge-18.0.1}/PKG-INFO +2 -2
  2. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/setup.py +2 -2
  3. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +8 -5
  4. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/models/__init__.py +1 -1
  5. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/models/base_api_response.py +15 -0
  6. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/models/base_data_model.py +1 -0
  7. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/services/__init__.py +5 -1
  8. ipulse_shared_core_ftredge-18.0.1/src/ipulse_shared_core_ftredge/services/charging_processors.py +350 -0
  9. ipulse_shared_core_ftredge-15.0.1/src/ipulse_shared_core_ftredge/services/credit_service.py → ipulse_shared_core_ftredge-18.0.1/src/ipulse_shared_core_ftredge/services/charging_service.py +11 -11
  10. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1/src/ipulse_shared_core_ftredge.egg-info}/PKG-INFO +2 -2
  11. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge.egg-info/SOURCES.txt +2 -1
  12. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +1 -1
  13. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/LICENCE +0 -0
  14. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/README.md +0 -0
  15. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/pyproject.toml +0 -0
  16. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/setup.cfg +0 -0
  17. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/__init__.py +0 -0
  18. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/cache/__init__.py +0 -0
  19. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/cache/shared_cache.py +0 -0
  20. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
  21. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +0 -0
  22. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/dependencies/auth_protected_router.py +0 -0
  23. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/dependencies/firestore_client.py +0 -0
  24. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/models/organization_profile.py +0 -0
  25. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/models/subscription.py +0 -0
  26. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/models/user_auth.py +0 -0
  27. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/models/user_profile.py +0 -0
  28. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/models/user_profile_update.py +0 -0
  29. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/models/user_status.py +0 -0
  30. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/services/base_firestore_service.py +0 -0
  31. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/services/base_service_exceptions.py +0 -0
  32. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/services/cache_aware_firestore_service.py +0 -0
  33. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/services/fastapiservicemon.py +0 -0
  34. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/services/servicemon.py +0 -0
  35. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -0
  36. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/utils/custom_json_encoder.py +0 -0
  37. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/utils/json_encoder.py +0 -0
  38. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge.egg-info/dependency_links.txt +0 -0
  39. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge.egg-info/top_level.txt +0 -0
  40. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/tests/test_cache_aware_service.py +0 -0
  41. {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/tests/test_shared_cache.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipulse_shared_core_ftredge
3
- Version: 15.0.1
3
+ Version: 18.0.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,7 +13,7 @@ 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==6.5.1
16
+ Requires-Dist: ipulse_shared_base_ftredge==7.2.0
17
17
  Dynamic: author
18
18
  Dynamic: classifier
19
19
  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='15.0.1',
6
+ version='18.0.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==6.5.1',
15
+ 'ipulse_shared_base_ftredge==7.2.0',
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.',
@@ -140,10 +140,12 @@ async def get_userstatus(
140
140
  snapshot = await get_with_strict_timeout(user_ref, timeout)
141
141
 
142
142
  if not snapshot.exists:
143
+ # Log at DEBUG level since this might be expected for new users
144
+ logger.debug(f"User status document not found for user {user_uid} (document: {userstatus_id})")
143
145
  raise ResourceNotFoundError(
144
- resource_type="authorization userstatus",
146
+ resource_type="authz_for_apis>userstatus",
145
147
  resource_id=userstatus_id,
146
- additional_info={"user_uid": user_uid}
148
+ additional_info={"user_uid": user_uid, "context": "authorization"}
147
149
  )
148
150
 
149
151
  status_data = snapshot.to_dict()
@@ -153,7 +155,10 @@ async def get_userstatus(
153
155
  userstatus_cache.set(user_uid, status_data)
154
156
  return status_data, cache_used
155
157
 
156
- except TimeoutError as e:
158
+ except ResourceNotFoundError:
159
+ # Re-raise ResourceNotFoundError as-is - don't wrap in ServiceError
160
+ raise
161
+ except (TimeoutError, FirestoreTimeoutError) as e:
157
162
  logger.error(f"Timeout while fetching user status for {user_uid}: {str(e)}")
158
163
  raise ServiceError(
159
164
  operation="fetching user status for authz",
@@ -166,8 +171,6 @@ async def get_userstatus(
166
171
  "timeout_seconds": timeout
167
172
  }
168
173
  )
169
- except ResourceNotFoundError:
170
- raise
171
174
  except Exception as e:
172
175
  logger.error(f"Error fetching user status for {user_uid}: {str(e)}")
173
176
  raise ServiceError(
@@ -4,7 +4,7 @@ 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
7
- from .base_api_response import BaseAPIResponse , CustomJSONResponse
7
+ from .base_api_response import BaseAPIResponse , CustomJSONResponse, CreditChargeableAPIResponse, UserCreditBalance, UpdatedUserCreditInfo
8
8
  from .base_data_model import BaseDataModel
9
9
 
10
10
 
@@ -11,6 +11,7 @@ T = TypeVar('T')
11
11
  class BaseAPIResponse(BaseModel, Generic[T]):
12
12
  model_config = ConfigDict(arbitrary_types_allowed=True)
13
13
  success: bool
14
+ chargeable: bool = False # Added chargeable attribute
14
15
  data: Optional[T] = None
15
16
  message: Optional[str] = None
16
17
  error: Optional[str] = None
@@ -19,6 +20,20 @@ class BaseAPIResponse(BaseModel, Generic[T]):
19
20
  "timestamp": dt.datetime.now(dt.timezone.utc).isoformat()
20
21
  }
21
22
 
23
+ class UserCreditBalance(BaseModel):
24
+ sbscrptn_based_insight_credits: float
25
+ extra_insight_credits: float
26
+
27
+ class UpdatedUserCreditInfo(BaseModel):
28
+ charge_attempted: bool
29
+ charge_successful: bool
30
+ cost_incurred: float
31
+ items_processed_for_charge: int
32
+ user_balance: UserCreditBalance
33
+
34
+ class CreditChargeableAPIResponse(BaseAPIResponse[T], Generic[T]):
35
+ updated_user_credit_info: Optional[UpdatedUserCreditInfo] = None
36
+
22
37
  class PaginatedAPIResponse(BaseAPIResponse, Generic[T]):
23
38
  total_count: int
24
39
  page: int
@@ -1,6 +1,7 @@
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
4
5
  from pydantic import BaseModel, Field, ConfigDict, field_validator
5
6
  import dateutil.parser
6
7
 
@@ -7,8 +7,12 @@ from ipulse_shared_core_ftredge.services.servicemon import Servicemon
7
7
  from ipulse_shared_core_ftredge.services.base_firestore_service import BaseFirestoreService
8
8
  from ipulse_shared_core_ftredge.services.cache_aware_firestore_service import CacheAwareFirestoreService
9
9
 
10
+ from ipulse_shared_core_ftredge.services.charging_processors import (ChargingProcessor)
11
+ from ipulse_shared_core_ftredge.services.charging_service import ChargingService
12
+
10
13
  __all__ = [
11
14
  'AuthorizationError', 'BaseServiceException', 'ServiceError', 'ValidationError',
12
15
  'ResourceNotFoundError', 'BaseFirestoreService',
13
- 'CacheAwareFirestoreService'
16
+ 'CacheAwareFirestoreService', 'Servicemon',
17
+ 'ChargingProcessor'
14
18
  ]
@@ -0,0 +1,350 @@
1
+ """Reusable credit checking and charging utilities for services."""
2
+ import os
3
+ from typing import Dict, Any, List, Optional, Callable, Awaitable, TypeVar
4
+ from ipulse_shared_core_ftredge.services.charging_service import ChargingService, ValidationError
5
+ import logging
6
+
7
+ T = TypeVar('T')
8
+
9
+ class ChargingProcessor:
10
+ """Handles credit checking and charging for both single item and batch access."""
11
+
12
+ def __init__(self, charging_service: ChargingService, logger: logging.Logger):
13
+ self.charging_service = charging_service
14
+ self.logger = logger
15
+
16
+ async def process_single_item_charging(
17
+ self,
18
+ user_uid: str,
19
+ item_id: str,
20
+ get_cost_func: Callable[[], Awaitable[Optional[float]]],
21
+ pre_fetched_credits: Optional[Dict[str, float]] = None,
22
+ operation_description: str = "Resource access"
23
+ ) -> Dict[str, Any]:
24
+ """
25
+ Process credit check and charging for a single item.
26
+
27
+ Args:
28
+ user_uid: User's UID
29
+ item_id: ID of the item being accessed
30
+ get_cost_func: Async function that returns the cost for the item
31
+ pre_fetched_credits: Optional pre-fetched credit information
32
+ operation_description: Description for the charging operation
33
+
34
+ Returns:
35
+ Dict with keys:
36
+ - access_granted: bool
37
+ - charge_successful: bool (only meaningful if access_granted is True)
38
+ - cost: Optional[float]
39
+ - reason: str (explanation if access denied)
40
+ - updated_user_credits: Optional[Dict] (credits after charging, if applicable)
41
+ """
42
+ self.logger.info(f"Processing single item credit check for user {user_uid}, item {item_id}")
43
+ updated_user_credits = None # Initialize
44
+
45
+ try:
46
+ # Get the credit cost for this item
47
+ credit_cost = await get_cost_func()
48
+
49
+ # If item is free or cost not configured, allow access
50
+ if credit_cost is None or credit_cost <= 0:
51
+ if credit_cost is None:
52
+ self.logger.info(f"Item {item_id} has no configured credit cost, treating as free.")
53
+
54
+ # For free items, provide current credits if available
55
+ if pre_fetched_credits:
56
+ updated_user_credits = pre_fetched_credits
57
+ elif self.charging_service: # Attempt to get current credits if not pre-fetched
58
+ try:
59
+ _, current_user_credits_from_verify = await self.charging_service.verify_credits(user_uid, 0, None)
60
+ updated_user_credits = current_user_credits_from_verify
61
+ except Exception: # pylint: disable=broad-except
62
+ self.logger.warning(f"Could not fetch current credits for user {user_uid} for free item.")
63
+
64
+ return {
65
+ 'access_granted': True,
66
+ 'charge_successful': True, # No charge needed
67
+ 'cost': credit_cost if credit_cost is not None else 0.0,
68
+ 'reason': 'free_item',
69
+ 'updated_user_credits': updated_user_credits
70
+ }
71
+
72
+ # Check for debug mode bypass
73
+ if os.getenv("BYPASS_CREDIT_CHECK", "").lower() == "true":
74
+ self.logger.info(f"Bypassing credit check for item {item_id} due to debug mode")
75
+ # Similar to free items, provide current credits if available
76
+ if pre_fetched_credits:
77
+ updated_user_credits = pre_fetched_credits
78
+ elif self.charging_service:
79
+ try:
80
+ _, current_user_credits_from_verify = await self.charging_service.verify_credits(user_uid, 0, None)
81
+ updated_user_credits = current_user_credits_from_verify
82
+ except Exception: # pylint: disable=broad-except
83
+ self.logger.warning(f"Could not fetch current credits for user {user_uid} during debug bypass.")
84
+ return {
85
+ 'access_granted': True,
86
+ 'charge_successful': True,
87
+ 'cost': credit_cost,
88
+ 'reason': 'debug_bypass',
89
+ 'updated_user_credits': updated_user_credits
90
+ }
91
+
92
+ # Verify credit service is available
93
+ if not self.charging_service:
94
+ self.logger.error("ChargingService not initialized.")
95
+ return {
96
+ 'access_granted': False,
97
+ 'charge_successful': False,
98
+ 'cost': credit_cost,
99
+ 'reason': 'service_unavailable',
100
+ 'updated_user_credits': None
101
+ }
102
+
103
+ # Verify user has enough credits
104
+ has_credits, current_user_credits_from_verify = await self.charging_service.verify_credits(
105
+ user_uid,
106
+ credit_cost,
107
+ pre_fetched_user_credits=pre_fetched_credits
108
+ )
109
+ # Store current credits from verification, might be used if charge fails
110
+ updated_user_credits = current_user_credits_from_verify
111
+
112
+
113
+ if not has_credits:
114
+ self.logger.warning(f"User {user_uid} has insufficient credits for item {item_id} (cost: {credit_cost})")
115
+ return {
116
+ 'access_granted': False,
117
+ 'charge_successful': False,
118
+ 'cost': credit_cost,
119
+ 'reason': 'insufficient_credits',
120
+ 'updated_user_credits': updated_user_credits # Return credits state at time of failure
121
+ }
122
+
123
+ # Charge the user - this now returns (bool, Optional[Dict])
124
+ charged, calculated_updated_credits = await self.charging_service.charge_credits_transaction(
125
+ user_uid,
126
+ credit_cost,
127
+ operation_description
128
+ )
129
+
130
+ # Use the credits returned by charge_credits if successful
131
+ if calculated_updated_credits is not None:
132
+ updated_user_credits = calculated_updated_credits
133
+
134
+
135
+ return {
136
+ 'access_granted': True, # Access granted because verify_credits passed
137
+ 'charge_successful': charged,
138
+ 'cost': credit_cost,
139
+ 'reason': 'charged' if charged else 'charge_failed',
140
+ 'updated_user_credits': updated_user_credits
141
+ }
142
+
143
+ except ValidationError as ve:
144
+ self.logger.error(f"Validation error for item {item_id}, user {user_uid}: {str(ve)}")
145
+ # Try to get current credits to return
146
+ if self.charging_service:
147
+ try:
148
+ _, updated_user_credits = await self.charging_service.verify_credits(user_uid, 0, pre_fetched_credits)
149
+ except Exception: # pylint: disable=broad-except
150
+ pass # Keep updated_user_credits as None
151
+ ve.additional_info = ve.additional_info or {}
152
+ ve.additional_info['updated_user_credits'] = updated_user_credits
153
+ raise
154
+ except Exception as e:
155
+ self.logger.error(f"Unexpected error during credit processing for item {item_id}, user {user_uid}: {str(e)}", exc_info=True)
156
+ # Try to get current credits to return
157
+ current_user_credits_on_error = None
158
+ if self.charging_service:
159
+ try:
160
+ _, current_user_credits_on_error = await self.charging_service.verify_credits(user_uid, 0, pre_fetched_credits)
161
+ except Exception: # pylint: disable=broad-except
162
+ pass
163
+ return {
164
+ 'access_granted': False,
165
+ 'charge_successful': False,
166
+ 'cost': None, # Cost might not be determined if error was early
167
+ 'reason': f'error: {str(e)}',
168
+ 'updated_user_credits': current_user_credits_on_error
169
+ }
170
+ async def process_batch_items_charging(
171
+ self,
172
+ user_uid: str,
173
+ items: List[Dict[str, Any]],
174
+ pre_fetched_credits: Optional[Dict[str, float]] = None,
175
+ operation_description: str = "Batch resource access"
176
+ ) -> Dict[str, Any]:
177
+ """
178
+ Process credit check and charging for a batch of items.
179
+
180
+ Args:
181
+ user_uid: User's UID
182
+ items: List of dicts with keys: 'id', 'data', 'get_cost_func'
183
+ pre_fetched_credits: Optional pre-fetched credit information
184
+ operation_description: Description for the charging operation
185
+
186
+ Returns:
187
+ Dict with keys:
188
+ - accessible_items: List[Dict] (items user can access)
189
+ - charge_successful: bool
190
+ - total_cost: float
191
+ - paid_items_count: int
192
+ - free_items_count: int
193
+ - updated_user_credits: Optional[Dict] (credits after charging, if applicable)
194
+ """
195
+ self.logger.info(f"Processing batch credit check for user {user_uid}, {len(items)} items")
196
+ updated_user_credits = None # Initialize
197
+
198
+ if not items:
199
+ return {
200
+ 'accessible_items': [],
201
+ 'charge_successful': True,
202
+ 'total_cost': 0.0,
203
+ 'paid_items_count': 0,
204
+ 'free_items_count': 0,
205
+ 'updated_user_credits': None
206
+ }
207
+
208
+ # Separate free and paid items
209
+ free_items = []
210
+ paid_items = []
211
+ total_cost = 0.0
212
+
213
+ for item in items:
214
+ try:
215
+ cost = await item['get_cost_func']()
216
+ if cost is None or cost <= 0:
217
+ free_items.append(item)
218
+ else:
219
+ paid_items.append(item)
220
+ total_cost += cost
221
+ except Exception as cost_err:
222
+ self.logger.error(f"Error getting cost for item {item.get('id', 'unknown')}: {cost_err}")
223
+ free_items.append(item)
224
+
225
+ self.logger.info(f"User {user_uid}: {len(free_items)} free items, {len(paid_items)} paid items (total cost: {total_cost})")
226
+
227
+ # If no paid items, return all free items
228
+ if not paid_items:
229
+ if pre_fetched_credits:
230
+ updated_user_credits = pre_fetched_credits
231
+ elif self.charging_service:
232
+ try:
233
+ _, current_user_credits_from_verify = await self.charging_service.verify_credits(user_uid, 0, None)
234
+ updated_user_credits = current_user_credits_from_verify
235
+
236
+ except Exception: # pylint: disable=broad-except
237
+ self.logger.warning(f"Could not fetch current credits for user {user_uid} for free batch.")
238
+
239
+ return {
240
+ 'accessible_items': free_items,
241
+ 'charge_successful': True,
242
+ 'total_cost': 0.0,
243
+ 'paid_items_count': 0,
244
+ 'free_items_count': len(free_items),
245
+ 'updated_user_credits': updated_user_credits
246
+ }
247
+
248
+ # Check for debug mode bypass
249
+ if os.getenv("BYPASS_CREDIT_CHECK", "").lower() == "true":
250
+ self.logger.info(f"Bypassing credit check for {len(paid_items)} paid items due to debug mode")
251
+ if pre_fetched_credits:
252
+ updated_user_credits = pre_fetched_credits
253
+ elif self.charging_service:
254
+ try:
255
+ _, current_user_credits_from_verify = await self.charging_service.verify_credits(user_uid, 0, None)
256
+ updated_user_credits = current_user_credits_from_verify
257
+ except Exception: # pylint: disable=broad-except
258
+ self.logger.warning(f"Could not fetch current credits for user {user_uid} during debug bypass for batch.")
259
+
260
+ return {
261
+ 'accessible_items': free_items + paid_items,
262
+ 'charge_successful': True,
263
+ 'total_cost': total_cost,
264
+ 'paid_items_count': len(paid_items),
265
+ 'free_items_count': len(free_items),
266
+ 'updated_user_credits': updated_user_credits
267
+ }
268
+
269
+ # Verify credit service is available
270
+ if not self.charging_service:
271
+ self.logger.error("ChargingService not initialized for batch processing.")
272
+ return {
273
+ 'accessible_items': free_items,
274
+ 'charge_successful': False,
275
+ 'total_cost': total_cost,
276
+ 'paid_items_count': len(paid_items),
277
+ 'free_items_count': len(free_items),
278
+ 'updated_user_credits': None
279
+ }
280
+
281
+ try:
282
+ # Verify user has enough credits for total cost
283
+ has_credits, current_user_credits_from_verify = await self.charging_service.verify_credits(
284
+ user_uid,
285
+ total_cost,
286
+ pre_fetched_user_credits=pre_fetched_credits
287
+ )
288
+ updated_user_credits = current_user_credits_from_verify
289
+
290
+
291
+ if not has_credits:
292
+ self.logger.warning(f"User {user_uid} has insufficient credits for batch (cost: {total_cost}). Returning free items only.")
293
+ return {
294
+ 'accessible_items': free_items,
295
+ 'charge_successful': False,
296
+ 'total_cost': total_cost,
297
+ 'paid_items_count': len(paid_items),
298
+ 'free_items_count': len(free_items),
299
+ 'updated_user_credits': updated_user_credits # Return credits state at time of failure
300
+ }
301
+
302
+ # Charge the user for all paid items
303
+ charged, calculated_updated_credits = await self.charging_service.charge_credits_transaction(
304
+ user_uid,
305
+ total_cost,
306
+ f"{operation_description} ({len(paid_items)} items, total cost: {total_cost})"
307
+ )
308
+
309
+ if calculated_updated_credits is not None:
310
+ updated_user_credits = calculated_updated_credits
311
+
312
+
313
+ # Return all items (free + paid) since credits were verified (even if charge failed post-verification)
314
+ return {
315
+ 'accessible_items': free_items + paid_items,
316
+ 'charge_successful': charged,
317
+ 'total_cost': total_cost,
318
+ 'paid_items_count': len(paid_items),
319
+ 'free_items_count': len(free_items),
320
+ 'updated_user_credits': updated_user_credits
321
+ }
322
+
323
+ except ValidationError as ve:
324
+ self.logger.error(f"Validation error during batch credit check for user {user_uid}: {str(ve)}")
325
+ if self.charging_service:
326
+ try:
327
+ _, current_user_credits_from_verify = await self.charging_service.verify_credits(user_uid, 0, pre_fetched_credits)
328
+ updated_user_credits = current_user_credits_from_verify
329
+ except Exception: # pylint: disable=broad-except
330
+ pass
331
+ ve.additional_info = ve.additional_info or {}
332
+ ve.additional_info['updated_user_credits'] = updated_user_credits
333
+ raise
334
+ except Exception as e:
335
+ self.logger.error(f"Unexpected error during batch credit check for user {user_uid}: {str(e)}", exc_info=True)
336
+ current_credits_on_error = None
337
+ if self.charging_service:
338
+ try:
339
+ _, current_credits_on_error = await self.charging_service.verify_credits(user_uid, 0, pre_fetched_credits)
340
+ updated_user_credits = current_credits_on_error
341
+ except Exception: # pylint: disable=broad-except
342
+ pass
343
+ return {
344
+ 'accessible_items': free_items, # Only free items if error
345
+ 'charge_successful': False,
346
+ 'total_cost': total_cost, # This is the cost of paid items that were attempted
347
+ 'paid_items_count': len(paid_items),
348
+ 'free_items_count': len(free_items),
349
+ 'updated_user_credits': updated_user_credits
350
+ }
@@ -9,9 +9,9 @@ from ipulse_shared_core_ftredge.models.user_status import UserStatus
9
9
  # Default Firestore timeout if not provided by the consuming application
10
10
  DEFAULT_FIRESTORE_TIMEOUT = 15.0
11
11
 
12
- class CreditService:
12
+ class ChargingService:
13
13
  """
14
- Service class for credit operations.
14
+ Service class for charging operations.
15
15
  Designed to be project-agnostic and directly uses UserStatus model constants.
16
16
  """
17
17
 
@@ -22,7 +22,7 @@ class CreditService:
22
22
  firestore_timeout: float = DEFAULT_FIRESTORE_TIMEOUT
23
23
  ):
24
24
  """
25
- Initialize the credit service.
25
+ Initialize the charging service.
26
26
 
27
27
  Args:
28
28
  db: Firestore client.
@@ -37,7 +37,7 @@ class CreditService:
37
37
  self.timeout = firestore_timeout
38
38
 
39
39
  self.logger.info(
40
- f"CreditService initialized using UserStatus constants. Collection: {self.users_status_collection_name}, "
40
+ f"ChargingService initialized using UserStatus constants. Collection: {self.users_status_collection_name}, "
41
41
  f"Doc Prefix: {self.user_status_doc_prefix}, Timeout: {self.timeout}s"
42
42
  )
43
43
 
@@ -93,13 +93,13 @@ class CreditService:
93
93
  f"(subscription: {subscription_credits}, extra: {extra_credits})"
94
94
  )
95
95
 
96
- userstatus_data_to_return = {
96
+ user_credits = {
97
97
  "sbscrptn_based_insight_credits": subscription_credits,
98
98
  "extra_insight_credits": extra_credits
99
99
  }
100
100
 
101
101
  has_enough_credits = total_credits >= required_credits_for_resource
102
- return has_enough_credits, userstatus_data_to_return
102
+ return has_enough_credits, user_credits
103
103
 
104
104
  try:
105
105
  self.logger.info(
@@ -118,12 +118,12 @@ class CreditService:
118
118
 
119
119
  has_enough_credits = total_credits >= required_credits_for_resource
120
120
 
121
- userstatus_data_to_return = {
121
+ user_credits = {
122
122
  "sbscrptn_based_insight_credits": subscription_credits,
123
123
  "extra_insight_credits": extra_credits
124
124
  }
125
125
 
126
- return has_enough_credits, userstatus_data_to_return
126
+ return has_enough_credits, user_credits
127
127
 
128
128
  except ResourceNotFoundError:
129
129
  self.logger.warning(f"User status not found for {user_uid} in {self.users_status_collection_name}. Assuming no credits.")
@@ -138,7 +138,7 @@ class CreditService:
138
138
  additional_info={"credits_to_charge": required_credits_for_resource}
139
139
  ) from e
140
140
 
141
- async def charge_credits(self, user_uid: str, credits_to_charge: Optional[float], operation_details: str) -> Tuple[bool, Optional[Dict[str, float]]]:
141
+ async def charge_credits_transaction(self, user_uid: str, credits_to_charge: Optional[float], operation_details: str) -> Tuple[bool, Optional[Dict[str, float]]]:
142
142
  """
143
143
  Charge a user's credits for an operation.
144
144
 
@@ -228,9 +228,9 @@ class CreditService:
228
228
  new_subscription_credits = current_subscription_credits - subscription_credits_deducted
229
229
  new_extra_credits = current_extra_credits - extra_credits_deducted
230
230
 
231
- update_data = {
231
+ update_data: Dict[str, Any] = {
232
232
  "updated_at": datetime.now(timezone.utc).isoformat(),
233
- "updated_by": "credit_service"
233
+ "updated_by": "charging_service__charge_credits_transaction" # Static identifier for this operation
234
234
  }
235
235
 
236
236
  if subscription_credits_deducted > 0:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipulse_shared_core_ftredge
3
- Version: 15.0.1
3
+ Version: 18.0.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,7 +13,7 @@ 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==6.5.1
16
+ Requires-Dist: ipulse_shared_base_ftredge==7.2.0
17
17
  Dynamic: author
18
18
  Dynamic: classifier
19
19
  Dynamic: home-page
@@ -28,7 +28,8 @@ src/ipulse_shared_core_ftredge/services/__init__.py
28
28
  src/ipulse_shared_core_ftredge/services/base_firestore_service.py
29
29
  src/ipulse_shared_core_ftredge/services/base_service_exceptions.py
30
30
  src/ipulse_shared_core_ftredge/services/cache_aware_firestore_service.py
31
- src/ipulse_shared_core_ftredge/services/credit_service.py
31
+ src/ipulse_shared_core_ftredge/services/charging_processors.py
32
+ src/ipulse_shared_core_ftredge/services/charging_service.py
32
33
  src/ipulse_shared_core_ftredge/services/fastapiservicemon.py
33
34
  src/ipulse_shared_core_ftredge/services/servicemon.py
34
35
  src/ipulse_shared_core_ftredge/utils/__init__.py
@@ -2,4 +2,4 @@ pydantic[email]~=2.5
2
2
  python-dateutil~=2.8
3
3
  fastapi~=0.115.8
4
4
  pytest
5
- ipulse_shared_base_ftredge==6.5.1
5
+ ipulse_shared_base_ftredge==7.2.0