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.
- {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
- {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/setup.py +2 -2
- {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
- {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/models/__init__.py +1 -1
- {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
- {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
- {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/services/__init__.py +5 -1
- ipulse_shared_core_ftredge-18.0.1/src/ipulse_shared_core_ftredge/services/charging_processors.py +350 -0
- 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
- {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
- {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
- {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
- {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/LICENCE +0 -0
- {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/README.md +0 -0
- {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/pyproject.toml +0 -0
- {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/setup.cfg +0 -0
- {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/__init__.py +0 -0
- {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/cache/__init__.py +0 -0
- {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
- {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
- {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
- {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
- {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
- {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
- {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/models/subscription.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/services/fastapiservicemon.py +0 -0
- {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/services/servicemon.py +0 -0
- {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -0
- {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
- {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
- {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
- {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
- {ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/tests/test_cache_aware_service.py +0 -0
- {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:
|
|
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==
|
|
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='
|
|
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==
|
|
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="
|
|
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
|
|
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
|
|
@@ -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
|
]
|
ipulse_shared_core_ftredge-18.0.1/src/ipulse_shared_core_ftredge/services/charging_processors.py
ADDED
|
@@ -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
|
|
12
|
+
class ChargingService:
|
|
13
13
|
"""
|
|
14
|
-
Service class for
|
|
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
|
|
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"
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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": "
|
|
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:
|
|
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==
|
|
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/
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ipulse_shared_core_ftredge-15.0.1 → ipulse_shared_core_ftredge-18.0.1}/tests/test_shared_cache.py
RENAMED
|
File without changes
|