ipulse-shared-core-ftredge 24.2.1__py3-none-any.whl → 26.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/dependencies/__init__.py +3 -1
- ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +0 -2
- ipulse_shared_core_ftredge/dependencies/authz_credit_extraction.py +67 -0
- ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +6 -6
- ipulse_shared_core_ftredge/models/__init__.py +3 -1
- ipulse_shared_core_ftredge/models/base_api_response.py +6 -43
- ipulse_shared_core_ftredge/models/credit_api_response.py +26 -0
- ipulse_shared_core_ftredge/models/custom_json_response.py +32 -0
- ipulse_shared_core_ftredge/models/user/user_subscription.py +7 -7
- ipulse_shared_core_ftredge/models/user/userstatus.py +1 -1
- ipulse_shared_core_ftredge/services/charging_processors.py +15 -15
- ipulse_shared_core_ftredge/services/user/__init__.py +1 -0
- ipulse_shared_core_ftredge/services/user/user_charging_operations.py +721 -0
- ipulse_shared_core_ftredge/services/user/user_core_service.py +123 -20
- ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +42 -52
- ipulse_shared_core_ftredge/services/user/user_subscription_operations.py +1 -1
- ipulse_shared_core_ftredge/services/user/userstatus_operations.py +1 -1
- ipulse_shared_core_ftredge/services/user_charging_service.py +19 -19
- {ipulse_shared_core_ftredge-24.2.1.dist-info → ipulse_shared_core_ftredge-26.1.1.dist-info}/METADATA +1 -1
- {ipulse_shared_core_ftredge-24.2.1.dist-info → ipulse_shared_core_ftredge-26.1.1.dist-info}/RECORD +23 -19
- {ipulse_shared_core_ftredge-24.2.1.dist-info → ipulse_shared_core_ftredge-26.1.1.dist-info}/WHEEL +0 -0
- {ipulse_shared_core_ftredge-24.2.1.dist-info → ipulse_shared_core_ftredge-26.1.1.dist-info}/licenses/LICENCE +0 -0
- {ipulse_shared_core_ftredge-24.2.1.dist-info → ipulse_shared_core_ftredge-26.1.1.dist-info}/top_level.txt +0 -0
|
@@ -28,6 +28,7 @@ from .user_subscription_operations import UsersubscriptionOperations
|
|
|
28
28
|
from .user_permissions_operations import UserpermissionsOperations
|
|
29
29
|
from .userauth_operations import UserauthOperations
|
|
30
30
|
from .user_multistep_operations import UsermultistepOperations
|
|
31
|
+
from .user_charging_operations import UserChargingOperations
|
|
31
32
|
from ..catalog.catalog_usertype_service import CatalogUserTypeService
|
|
32
33
|
from ..catalog.catalog_subscriptionplan_service import CatalogSubscriptionPlanService
|
|
33
34
|
|
|
@@ -47,7 +48,8 @@ class UserCoreService:
|
|
|
47
48
|
logger: Optional[logging.Logger] = None,
|
|
48
49
|
default_timeout: float = 10.0,
|
|
49
50
|
profile_collection: Optional[str] = None,
|
|
50
|
-
status_collection: Optional[str] = None
|
|
51
|
+
status_collection: Optional[str] = None,
|
|
52
|
+
bypass_credit_check: bool = False
|
|
51
53
|
):
|
|
52
54
|
"""
|
|
53
55
|
Initialize the Enhanced UserCoreService
|
|
@@ -58,8 +60,7 @@ class UserCoreService:
|
|
|
58
60
|
default_timeout: Default timeout for Firestore operations
|
|
59
61
|
profile_collection: Collection name for user profiles
|
|
60
62
|
status_collection: Collection name for user statuses
|
|
61
|
-
|
|
62
|
-
user_types_collection: Collection name for user types
|
|
63
|
+
bypass_credit_check: If True, bypasses credit checks for debugging/testing
|
|
63
64
|
"""
|
|
64
65
|
self.db = firestore_client
|
|
65
66
|
self.logger = logger or logging.getLogger(__name__)
|
|
@@ -87,19 +88,27 @@ class UserCoreService:
|
|
|
87
88
|
status_collection=self.status_collection_name,
|
|
88
89
|
)
|
|
89
90
|
|
|
90
|
-
self.
|
|
91
|
+
self.usepermission_ops = UserpermissionsOperations(
|
|
91
92
|
userstatus_ops=self.userstatus_ops,
|
|
92
93
|
logger=self.logger
|
|
93
94
|
)
|
|
94
95
|
|
|
95
|
-
self.
|
|
96
|
+
self.user_subscription_ops = UsersubscriptionOperations(
|
|
96
97
|
firestore_client=self.db,
|
|
97
98
|
userstatus_ops=self.userstatus_ops,
|
|
98
|
-
permissions_ops=self.
|
|
99
|
+
permissions_ops=self.usepermission_ops,
|
|
99
100
|
logger=self.logger,
|
|
100
101
|
timeout=self.timeout
|
|
101
102
|
)
|
|
102
103
|
|
|
104
|
+
# Initialize charging operations
|
|
105
|
+
self.user_charging_ops = UserChargingOperations(
|
|
106
|
+
userstatus_ops=self.userstatus_ops,
|
|
107
|
+
logger=self.logger,
|
|
108
|
+
timeout=self.timeout,
|
|
109
|
+
bypass_credit_check=bypass_credit_check
|
|
110
|
+
)
|
|
111
|
+
|
|
103
112
|
# Initialize catalog services
|
|
104
113
|
self.catalog_usertype_service = CatalogUserTypeService(
|
|
105
114
|
firestore_client=self.db,
|
|
@@ -116,8 +125,8 @@ class UserCoreService:
|
|
|
116
125
|
userprofile_ops=self.userprofile_ops,
|
|
117
126
|
userstatus_ops=self.userstatus_ops,
|
|
118
127
|
userauth_ops=self.userauth_ops,
|
|
119
|
-
usersubscription_ops=self.
|
|
120
|
-
useriam_ops=self.
|
|
128
|
+
usersubscription_ops=self.user_subscription_ops,
|
|
129
|
+
useriam_ops=self.usepermission_ops,
|
|
121
130
|
catalog_usertype_service=self.catalog_usertype_service,
|
|
122
131
|
catalog_subscriptionplan_service=self.catalog_subscriptionplan_service,
|
|
123
132
|
logger=self.logger
|
|
@@ -502,7 +511,7 @@ class UserCoreService:
|
|
|
502
511
|
auto_renewal_end: Optional[datetime] = None
|
|
503
512
|
) -> UserSubscription:
|
|
504
513
|
"""Fetch a subscription plan from catalog and apply to user"""
|
|
505
|
-
return await self.
|
|
514
|
+
return await self.user_subscription_ops.fetch_subscriptionplan_and_apply_subscription_to_user(
|
|
506
515
|
user_uid=user_uid,
|
|
507
516
|
plan_id=plan_id,
|
|
508
517
|
updater_uid=updater_uid,
|
|
@@ -521,7 +530,7 @@ class UserCoreService:
|
|
|
521
530
|
auto_renewal_end: Optional[datetime] = None
|
|
522
531
|
) -> UserSubscription:
|
|
523
532
|
"""Apply a subscription plan directly to user (plan already fetched)"""
|
|
524
|
-
return await self.
|
|
533
|
+
return await self.user_subscription_ops.apply_subscriptionplan(
|
|
525
534
|
user_uid=user_uid,
|
|
526
535
|
subscriptionplan=subscriptionplan,
|
|
527
536
|
updater_uid=updater_uid,
|
|
@@ -532,11 +541,11 @@ class UserCoreService:
|
|
|
532
541
|
|
|
533
542
|
async def cancel_user_subscription(self, user_uid: str, updater_uid: str) -> bool:
|
|
534
543
|
"""Cancel a user's active subscription"""
|
|
535
|
-
return await self.
|
|
544
|
+
return await self.user_subscription_ops.cancel_user_subscription(user_uid=user_uid, updater_uid=updater_uid)
|
|
536
545
|
|
|
537
546
|
async def get_user_active_subscription(self, user_uid: str) -> Optional[UserSubscription]:
|
|
538
547
|
"""Get a user's active subscription"""
|
|
539
|
-
return await self.
|
|
548
|
+
return await self.user_subscription_ops.get_user_active_subscription(user_uid=user_uid)
|
|
540
549
|
|
|
541
550
|
async def update_user_subscription(
|
|
542
551
|
self,
|
|
@@ -545,7 +554,7 @@ class UserCoreService:
|
|
|
545
554
|
updater_uid: str
|
|
546
555
|
) -> Optional[UserSubscription]:
|
|
547
556
|
"""Update a user's subscription"""
|
|
548
|
-
return await self.
|
|
557
|
+
return await self.user_subscription_ops.update_user_subscription(user_uid=user_uid, subscription_updates=subscription_data, updater_uid=updater_uid)
|
|
549
558
|
|
|
550
559
|
async def downgrade_user_subscription_to_fallback_subscriptionplan(
|
|
551
560
|
self,
|
|
@@ -553,7 +562,7 @@ class UserCoreService:
|
|
|
553
562
|
reason: str = "subscription_expired"
|
|
554
563
|
) -> Optional[UserSubscription]:
|
|
555
564
|
"""Downgrade user subscription to fallback plan"""
|
|
556
|
-
return await self.
|
|
565
|
+
return await self.user_subscription_ops.downgrade_user_subscription_to_fallback_subscriptionplan(
|
|
557
566
|
user_uid=user_uid, reason=reason
|
|
558
567
|
)
|
|
559
568
|
|
|
@@ -568,15 +577,15 @@ class UserCoreService:
|
|
|
568
577
|
updater_uid: str
|
|
569
578
|
) -> bool:
|
|
570
579
|
"""Add a permission to a user (returns success boolean)"""
|
|
571
|
-
return await self.
|
|
580
|
+
return await self.usepermission_ops.add_permission_to_user(user_uid=user_uid, permission=permission, updater_uid=updater_uid)
|
|
572
581
|
|
|
573
582
|
async def get_permissions_of_user(self, user_uid: str) -> List[UserPermission]:
|
|
574
583
|
"""Get a user's permissions"""
|
|
575
|
-
return await self.
|
|
584
|
+
return await self.usepermission_ops.get_permissions_of_user(user_uid=user_uid)
|
|
576
585
|
|
|
577
586
|
async def remove_all_permissions_from_user(self, user_uid: str, updater_uid: str, source: Optional[str] = None) -> int:
|
|
578
587
|
"""Remove all permissions from a user"""
|
|
579
|
-
return await self.
|
|
588
|
+
return await self.usepermission_ops.remove_all_permissions_from_user(user_uid=user_uid, source=source, updater_uid=updater_uid)
|
|
580
589
|
|
|
581
590
|
async def remove_permission_from_user(
|
|
582
591
|
self,
|
|
@@ -588,7 +597,7 @@ class UserCoreService:
|
|
|
588
597
|
updater_uid: Optional[str] = None
|
|
589
598
|
) -> bool:
|
|
590
599
|
"""Remove specific permission(s) from a user based on filter criteria"""
|
|
591
|
-
return await self.
|
|
600
|
+
return await self.usepermission_ops.remove_permission_from_user(
|
|
592
601
|
user_uid=user_uid,
|
|
593
602
|
domain=domain,
|
|
594
603
|
permission_type=permission_type,
|
|
@@ -604,7 +613,7 @@ class UserCoreService:
|
|
|
604
613
|
iam_unit_type: Optional[IAMUnit] = None
|
|
605
614
|
) -> int:
|
|
606
615
|
"""Remove expired permissions from a user"""
|
|
607
|
-
return await self.
|
|
616
|
+
return await self.usepermission_ops.cleanup_expired_permissions_of_user(user_uid=user_uid, iam_unit_type=iam_unit_type, updater_uid=updater_uid)
|
|
608
617
|
|
|
609
618
|
async def get_bulk_users_with_permission(
|
|
610
619
|
self,
|
|
@@ -615,7 +624,7 @@ class UserCoreService:
|
|
|
615
624
|
valid_only: bool = True
|
|
616
625
|
) -> List[str]:
|
|
617
626
|
"""Get bulk users who have a specific permission"""
|
|
618
|
-
return await self.
|
|
627
|
+
return await self.usepermission_ops.get_bulk_users_with_permission(
|
|
619
628
|
domain=domain,
|
|
620
629
|
iam_unit_type=iam_unit_type,
|
|
621
630
|
permission_ref=permission_ref,
|
|
@@ -649,6 +658,100 @@ class UserCoreService:
|
|
|
649
658
|
updater_uid=updater_uid
|
|
650
659
|
)
|
|
651
660
|
|
|
661
|
+
######################################################################
|
|
662
|
+
####################### User Charging Operations ####################
|
|
663
|
+
######################################################################
|
|
664
|
+
|
|
665
|
+
async def verify_user_has_enough_credits(
|
|
666
|
+
self,
|
|
667
|
+
user_uid: str,
|
|
668
|
+
required_credits: float,
|
|
669
|
+
credits_extracted_from_authz_response: Optional[Dict[str, float]] = None
|
|
670
|
+
) -> Tuple[bool, Dict[str, Any]]:
|
|
671
|
+
"""
|
|
672
|
+
Verify if user has sufficient credits for a resource.
|
|
673
|
+
Delegates to UserChargingOperations.
|
|
674
|
+
"""
|
|
675
|
+
return await self.user_charging_ops.verify_enough_credits(
|
|
676
|
+
user_uid=user_uid,
|
|
677
|
+
required_credits_for_resource=required_credits,
|
|
678
|
+
credits_extracted_from_authz_response=credits_extracted_from_authz_response
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
async def debit_credits_from_user_transaction(
|
|
682
|
+
self,
|
|
683
|
+
user_uid: str,
|
|
684
|
+
credits_to_take: float,
|
|
685
|
+
operation_details: str
|
|
686
|
+
) -> Tuple[bool, Optional[Dict[str, float]]]:
|
|
687
|
+
"""
|
|
688
|
+
Charge credits from user account using Firestore transaction.
|
|
689
|
+
Delegates to UserChargingOperations.
|
|
690
|
+
"""
|
|
691
|
+
return await self.user_charging_ops.debit_credits_transaction(
|
|
692
|
+
user_uid=user_uid,
|
|
693
|
+
credits_to_take=credits_to_take,
|
|
694
|
+
operation_details=operation_details
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
async def credit_credits_to_user_transaction(
|
|
698
|
+
self,
|
|
699
|
+
user_uid: str,
|
|
700
|
+
extra_credits_to_add: float = 0.0,
|
|
701
|
+
subscription_credits_to_add: float = 0.0,
|
|
702
|
+
reason: str = "",
|
|
703
|
+
updater_uid: str = "system"
|
|
704
|
+
) -> Tuple[bool, Optional[Dict[str, float]]]:
|
|
705
|
+
"""
|
|
706
|
+
Add credits to user account (extra and/or subscription credits).
|
|
707
|
+
Delegates to UserChargingOperations.
|
|
708
|
+
"""
|
|
709
|
+
return await self.user_charging_ops.credit_credits_transaction(
|
|
710
|
+
user_uid=user_uid,
|
|
711
|
+
extra_credits_to_add=extra_credits_to_add,
|
|
712
|
+
subscription_credits_to_add=subscription_credits_to_add,
|
|
713
|
+
reason=reason,
|
|
714
|
+
updater_uid=updater_uid
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
async def process_single_item_charge(
|
|
718
|
+
self,
|
|
719
|
+
user_uid: str,
|
|
720
|
+
item_id: str,
|
|
721
|
+
get_cost_func,
|
|
722
|
+
credits_extracted_from_authz_response: Optional[Dict[str, float]] = None,
|
|
723
|
+
operation_description: str = "Resource access"
|
|
724
|
+
) -> Dict[str, Any]:
|
|
725
|
+
"""
|
|
726
|
+
Process credit check and charging for a single item.
|
|
727
|
+
Delegates to UserChargingOperations.
|
|
728
|
+
"""
|
|
729
|
+
return await self.user_charging_ops.process_single_item_charging(
|
|
730
|
+
user_uid=user_uid,
|
|
731
|
+
item_id=item_id,
|
|
732
|
+
get_cost_func=get_cost_func,
|
|
733
|
+
credits_extracted_from_authz_response=credits_extracted_from_authz_response,
|
|
734
|
+
operation_description=operation_description
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
async def process_batch_charge(
|
|
738
|
+
self,
|
|
739
|
+
user_uid: str,
|
|
740
|
+
items: List[Dict[str, Any]],
|
|
741
|
+
credits_extracted_from_authz_response: Optional[Dict[str, float]] = None,
|
|
742
|
+
operation_description: str = "Batch resource access"
|
|
743
|
+
) -> Dict[str, Any]:
|
|
744
|
+
"""
|
|
745
|
+
Process credit check and charging for batch items.
|
|
746
|
+
Delegates to UserChargingOperations.
|
|
747
|
+
"""
|
|
748
|
+
return await self.user_charging_ops.process_batch_items_charging(
|
|
749
|
+
user_uid=user_uid,
|
|
750
|
+
items=items,
|
|
751
|
+
credits_extracted_from_authz_response=credits_extracted_from_authz_response,
|
|
752
|
+
operation_description=operation_description
|
|
753
|
+
)
|
|
754
|
+
|
|
652
755
|
######################################################################
|
|
653
756
|
####################### User Subscription/Status Review #############
|
|
654
757
|
######################################################################
|
|
@@ -7,6 +7,7 @@ Firebase Auth, UserProfile, and UserStatus in coordinated transactions.
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import logging
|
|
9
9
|
from typing import Dict, Any, Optional, List, Tuple, cast
|
|
10
|
+
from google.cloud import firestore
|
|
10
11
|
from ipulse_shared_base_ftredge.enums import IAMUserType
|
|
11
12
|
from ...models import UserProfile, UserStatus, UserAuth, UserAuthCreateNew, UserType
|
|
12
13
|
from .userauth_operations import UserauthOperations
|
|
@@ -48,43 +49,6 @@ class UsermultistepOperations:
|
|
|
48
49
|
self.catalog_subscriptionplan_service = catalog_subscriptionplan_service
|
|
49
50
|
self.logger = logger or logging.getLogger(__name__)
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
async def _rollback_user_creation(
|
|
54
|
-
self,
|
|
55
|
-
user_uid: Optional[str],
|
|
56
|
-
profile_created: bool,
|
|
57
|
-
status_created: bool,
|
|
58
|
-
error_context: str
|
|
59
|
-
) -> None:
|
|
60
|
-
"""Rollback user creation on failure."""
|
|
61
|
-
if not user_uid:
|
|
62
|
-
self.logger.error("Rollback cannot proceed: user_uid is None. Context: %s", error_context)
|
|
63
|
-
return
|
|
64
|
-
|
|
65
|
-
self.logger.warning("Rolling back user creation for UID: %s. Context: %s", user_uid, error_context)
|
|
66
|
-
|
|
67
|
-
# Attempt to clean up Firestore documents if they were created
|
|
68
|
-
if profile_created:
|
|
69
|
-
try:
|
|
70
|
-
await self.userprofile_ops.delete_userprofile(user_uid, "rollback", archive=False)
|
|
71
|
-
self.logger.info("Successfully deleted orphaned UserProfile for: %s", user_uid)
|
|
72
|
-
except Exception as del_prof_e:
|
|
73
|
-
self.logger.error("Failed to delete orphaned UserProfile for %s: %s", user_uid, del_prof_e)
|
|
74
|
-
if status_created:
|
|
75
|
-
try:
|
|
76
|
-
await self.userstatus_ops.delete_userstatus(user_uid, "rollback", archive=False)
|
|
77
|
-
self.logger.info("Successfully deleted orphaned UserStatus for: %s", user_uid)
|
|
78
|
-
except Exception as del_stat_e:
|
|
79
|
-
self.logger.error("Failed to delete orphaned UserStatus for %s: %s", user_uid, del_stat_e)
|
|
80
|
-
|
|
81
|
-
# Attempt to delete the orphaned Firebase Auth user
|
|
82
|
-
try:
|
|
83
|
-
await self.userauth_ops.delete_userauth(user_uid)
|
|
84
|
-
self.logger.info("Successfully deleted orphaned Firebase Auth user: %s", user_uid)
|
|
85
|
-
except Exception as delete_e:
|
|
86
|
-
self.logger.error("Failed to delete orphaned Firebase Auth user %s: %s", user_uid, delete_e, exc_info=True)
|
|
87
|
-
|
|
88
52
|
def _validate_usertype_consistency(
|
|
89
53
|
self,
|
|
90
54
|
userprofile: UserProfile,
|
|
@@ -158,8 +122,6 @@ class UsermultistepOperations:
|
|
|
158
122
|
Returns:
|
|
159
123
|
Tuple of (user_uid, userprofile, userstatus)
|
|
160
124
|
"""
|
|
161
|
-
profile_created = False
|
|
162
|
-
status_created = False
|
|
163
125
|
firebase_user_uid = None
|
|
164
126
|
|
|
165
127
|
# Validate that UserProfile and UserStatus have matching user_uid
|
|
@@ -211,15 +173,39 @@ class UsermultistepOperations:
|
|
|
211
173
|
if existing_userauth and existing_userauth.custom_claims:
|
|
212
174
|
self._validate_usertype_consistency(userprofile, existing_userauth.custom_claims)
|
|
213
175
|
|
|
214
|
-
# Step 2: Create UserProfile and UserStatus
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
176
|
+
# Step 2: Create UserProfile and UserStatus atomically using transaction
|
|
177
|
+
try:
|
|
178
|
+
# Execute transaction with nested transactional function
|
|
179
|
+
@firestore.transactional
|
|
180
|
+
def create_user_documents(transaction_obj):
|
|
181
|
+
"""Create both UserProfile and UserStatus documents atomically"""
|
|
182
|
+
# Create UserProfile document reference
|
|
183
|
+
profile_ref = self.userprofile_ops.db_service.db.collection(
|
|
184
|
+
self.userprofile_ops.profile_collection_name
|
|
185
|
+
).document(f"{UserProfile.OBJ_REF}_{user_uid}")
|
|
186
|
+
|
|
187
|
+
# Create UserStatus document reference
|
|
188
|
+
status_ref = self.userstatus_ops._status_db_service.db.collection(
|
|
189
|
+
self.userstatus_ops.status_collection_name
|
|
190
|
+
).document(f"{UserStatus.OBJ_REF}_{user_uid}")
|
|
191
|
+
|
|
192
|
+
# Set both documents in transaction
|
|
193
|
+
transaction_obj.set(profile_ref, final_userprofile.model_dump(exclude_none=True))
|
|
194
|
+
transaction_obj.set(status_ref, final_userstatus.model_dump(exclude_none=True))
|
|
195
|
+
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
# Execute the transaction
|
|
199
|
+
transaction = self.userprofile_ops.db_service.db.transaction()
|
|
200
|
+
success = create_user_documents(transaction)
|
|
201
|
+
|
|
202
|
+
if success:
|
|
203
|
+
self.logger.info("Successfully created UserProfile and UserStatus atomically for user: %s (with %d IAM permissions)",
|
|
204
|
+
user_uid, len(final_userstatus.iam_permissions))
|
|
205
|
+
|
|
206
|
+
except Exception as transaction_error:
|
|
207
|
+
self.logger.error("Failed to create user documents atomically for %s: %s", user_uid, str(transaction_error))
|
|
208
|
+
raise UserCreationError(f"Atomic user document creation failed: {str(transaction_error)}") from transaction_error
|
|
223
209
|
|
|
224
210
|
# Step 3: Fetch final state to return
|
|
225
211
|
final_profile = await self.userprofile_ops.get_userprofile(user_uid)
|
|
@@ -232,10 +218,14 @@ class UsermultistepOperations:
|
|
|
232
218
|
return user_uid, final_profile, final_status
|
|
233
219
|
|
|
234
220
|
except Exception as e:
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
221
|
+
# Only cleanup Firebase Auth user if one was created (Firestore docs auto-rollback via transaction)
|
|
222
|
+
if firebase_user_uid:
|
|
223
|
+
try:
|
|
224
|
+
await self.userauth_ops.delete_userauth(firebase_user_uid)
|
|
225
|
+
self.logger.info("Successfully deleted orphaned Firebase Auth user: %s", firebase_user_uid)
|
|
226
|
+
except Exception as delete_e:
|
|
227
|
+
self.logger.error("Failed to delete orphaned Firebase Auth user %s: %s", firebase_user_uid, delete_e, exc_info=True)
|
|
228
|
+
|
|
239
229
|
raise UserCreationError(f"Failed to create user from models: {str(e)}") from e
|
|
240
230
|
|
|
241
231
|
async def create_user_from_manual_usertype(
|
|
@@ -86,7 +86,7 @@ class UsersubscriptionOperations:
|
|
|
86
86
|
plan_name=plan_name_enum,
|
|
87
87
|
plan_version=plan.plan_version,
|
|
88
88
|
plan_id=plan.id or f"{plan.plan_name}_{plan.plan_version}",
|
|
89
|
-
|
|
89
|
+
cycle_start_datetime=start_date,
|
|
90
90
|
cycle_end_datetime=end_date,
|
|
91
91
|
validity_time_length=plan.plan_validity_cycle_length,
|
|
92
92
|
validity_time_unit=plan.plan_validity_cycle_unit,
|
|
@@ -361,7 +361,7 @@ class UserstatusOperations:
|
|
|
361
361
|
# Create new subscription with updated cycle start date
|
|
362
362
|
subscription_dict = subscription.model_dump()
|
|
363
363
|
subscription_dict.update({
|
|
364
|
-
'
|
|
364
|
+
'cycle_start_datetime': new_cycle_start,
|
|
365
365
|
'cycle_end_datetime': None, # Let the model auto-calculate this
|
|
366
366
|
'updated_at': now,
|
|
367
367
|
'updated_by': f"UserstatusOperations.auto_renew:{updater_uid}"
|
|
@@ -37,11 +37,11 @@ class UserChargingService:
|
|
|
37
37
|
self.timeout = firestore_timeout
|
|
38
38
|
|
|
39
39
|
self.logger.info(
|
|
40
|
-
f"
|
|
40
|
+
f"UserChargingService initialized using UserStatus constants. Collection: {self.users_status_collection_name}, "
|
|
41
41
|
f"Doc Prefix: {self.userstatus_doc_prefix}, Timeout: {self.timeout}s"
|
|
42
42
|
)
|
|
43
43
|
|
|
44
|
-
async def
|
|
44
|
+
async def verify_enough_credits(
|
|
45
45
|
self,
|
|
46
46
|
user_uid: str,
|
|
47
47
|
required_credits_for_resource: float,
|
|
@@ -64,7 +64,7 @@ class UserChargingService:
|
|
|
64
64
|
ValidationError: If required_credits_for_resource is None (pricing not properly configured).
|
|
65
65
|
"""
|
|
66
66
|
self.logger.info(
|
|
67
|
-
f"
|
|
67
|
+
f"verify_enough_credits called for user {user_uid}, "
|
|
68
68
|
f"required_credits={required_credits_for_resource}, "
|
|
69
69
|
f"pre_fetched_credits={pre_fetched_user_credits}"
|
|
70
70
|
)
|
|
@@ -135,16 +135,16 @@ class UserChargingService:
|
|
|
135
135
|
error=e,
|
|
136
136
|
resource_type="user_credits",
|
|
137
137
|
resource_id=user_uid,
|
|
138
|
-
additional_info={"
|
|
138
|
+
additional_info={"credits_to_take": required_credits_for_resource}
|
|
139
139
|
) from e
|
|
140
140
|
|
|
141
|
-
async def
|
|
141
|
+
async def debit_credits_transaction(self, user_uid: str, credits_to_take: 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
|
|
|
145
145
|
Args:
|
|
146
146
|
user_uid: The user's UID.
|
|
147
|
-
|
|
147
|
+
credits_to_take: The number of credits to charge.
|
|
148
148
|
operation_details: Details about the operation (for logging).
|
|
149
149
|
|
|
150
150
|
Returns:
|
|
@@ -153,9 +153,9 @@ class UserChargingService:
|
|
|
153
153
|
if charging was successful.
|
|
154
154
|
|
|
155
155
|
Raises:
|
|
156
|
-
ValidationError: If
|
|
156
|
+
ValidationError: If credits_to_take is None (pricing not properly configured).
|
|
157
157
|
"""
|
|
158
|
-
if
|
|
158
|
+
if credits_to_take is None:
|
|
159
159
|
self.logger.error(f"Credit cost is None for user {user_uid} (charge_credits), pricing not properly configured")
|
|
160
160
|
raise ValidationError(
|
|
161
161
|
resource_type="credit_cost",
|
|
@@ -164,7 +164,7 @@ class UserChargingService:
|
|
|
164
164
|
additional_info={"user_uid": user_uid}
|
|
165
165
|
)
|
|
166
166
|
|
|
167
|
-
if
|
|
167
|
+
if credits_to_take == 0:
|
|
168
168
|
self.logger.info(f"No credits to charge for user {user_uid}, operation: {operation_details}")
|
|
169
169
|
# If no charge, current credits are unchanged. Fetch them if needed by caller.
|
|
170
170
|
# For simplicity here, returning None for updated_credits as no transaction occurred.
|
|
@@ -194,10 +194,10 @@ class UserChargingService:
|
|
|
194
194
|
current_extra_credits = userstatus.get("extra_insight_credits", 0.0)
|
|
195
195
|
total_available_credits = current_subscription_credits + current_extra_credits
|
|
196
196
|
|
|
197
|
-
if total_available_credits <
|
|
197
|
+
if total_available_credits < credits_to_take:
|
|
198
198
|
self.logger.warning(
|
|
199
199
|
f"Insufficient credits for user {user_uid} during transaction: "
|
|
200
|
-
f"has {total_available_credits}, needs {
|
|
200
|
+
f"has {total_available_credits}, needs {credits_to_take}"
|
|
201
201
|
)
|
|
202
202
|
# Return current credits if charge fails due to insufficient funds
|
|
203
203
|
return False, {
|
|
@@ -206,16 +206,16 @@ class UserChargingService:
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
# Calculate how much to deduct from each type of credit
|
|
209
|
-
subscription_credits_deducted = min(current_subscription_credits,
|
|
210
|
-
remaining_charge =
|
|
209
|
+
subscription_credits_deducted = min(current_subscription_credits, credits_to_take)
|
|
210
|
+
remaining_charge = credits_to_take - subscription_credits_deducted
|
|
211
211
|
extra_credits_deducted = min(current_extra_credits, remaining_charge)
|
|
212
212
|
|
|
213
213
|
# This check should ideally not be needed if total_available_credits was sufficient,
|
|
214
214
|
# but as a safeguard:
|
|
215
|
-
if (subscription_credits_deducted + extra_credits_deducted) <
|
|
215
|
+
if (subscription_credits_deducted + extra_credits_deducted) < credits_to_take:
|
|
216
216
|
self.logger.error(
|
|
217
217
|
f"Credit calculation error for user {user_uid}. "
|
|
218
|
-
f"Required: {
|
|
218
|
+
f"Required: {credits_to_take}, Calculated deduction: {subscription_credits_deducted + extra_credits_deducted}"
|
|
219
219
|
)
|
|
220
220
|
# This case implies a logic flaw or race condition if not for the initial check.
|
|
221
221
|
# Return current credits as charge effectively failed.
|
|
@@ -230,7 +230,7 @@ class UserChargingService:
|
|
|
230
230
|
|
|
231
231
|
update_data: Dict[str, Any] = {
|
|
232
232
|
"updated_at": datetime.now(timezone.utc).isoformat(),
|
|
233
|
-
"updated_by": "
|
|
233
|
+
"updated_by": "charging_service__debit_credits_transaction" # Static identifier for this operation
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
if subscription_credits_deducted > 0:
|
|
@@ -253,12 +253,12 @@ class UserChargingService:
|
|
|
253
253
|
|
|
254
254
|
if success:
|
|
255
255
|
self.logger.info(
|
|
256
|
-
f"Successfully charged {
|
|
256
|
+
f"Successfully charged {credits_to_take} credits for user {user_uid}. "
|
|
257
257
|
f"Operation: {operation_details}. New balances: {updated_credits_dict}"
|
|
258
258
|
)
|
|
259
259
|
else:
|
|
260
260
|
self.logger.warning(
|
|
261
|
-
f"Failed to charge {
|
|
261
|
+
f"Failed to charge {credits_to_take} credits for user {user_uid} (transaction outcome). "
|
|
262
262
|
f"Operation: {operation_details}. Current balances: {updated_credits_dict}"
|
|
263
263
|
)
|
|
264
264
|
|
|
@@ -271,7 +271,7 @@ class UserChargingService:
|
|
|
271
271
|
error=e,
|
|
272
272
|
resource_type="user_credits",
|
|
273
273
|
resource_id=user_uid,
|
|
274
|
-
additional_info={"
|
|
274
|
+
additional_info={"credits_to_take": credits_to_take}
|
|
275
275
|
) from e
|
|
276
276
|
|
|
277
277
|
async def _get_userstatus(self, user_uid: str) -> Dict[str, Any]:
|
{ipulse_shared_core_ftredge-24.2.1.dist-info → ipulse_shared_core_ftredge-26.1.1.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ipulse_shared_core_ftredge
|
|
3
|
-
Version:
|
|
3
|
+
Version: 26.1.1
|
|
4
4
|
Summary: Shared Core models and Logger util for the Pulse platform project. Using AI for financial advisory and investment management.
|
|
5
5
|
Home-page: https://github.com/TheFutureEdge/ipulse_shared_core
|
|
6
6
|
Author: Russlan Ramdowar
|
{ipulse_shared_core_ftredge-24.2.1.dist-info → ipulse_shared_core_ftredge-26.1.1.dist-info}/RECORD
RENAMED
|
@@ -1,50 +1,54 @@
|
|
|
1
1
|
ipulse_shared_core_ftredge/__init__.py,sha256=-KbdF_YW8pgf7pVv9qh_cA1xrNm_B9zigHYDo7ZA4eU,42
|
|
2
2
|
ipulse_shared_core_ftredge/cache/__init__.py,sha256=i2fPojmZiBwAoY5ovnnnME9USl4bi8MRPYkAgEfACfI,136
|
|
3
3
|
ipulse_shared_core_ftredge/cache/shared_cache.py,sha256=BDJtkTsdfmVjKaUkbBXOhJ2Oib7Li0UCsPjWX7FLIPU,12940
|
|
4
|
-
ipulse_shared_core_ftredge/dependencies/__init__.py,sha256=
|
|
5
|
-
ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py,sha256=
|
|
4
|
+
ipulse_shared_core_ftredge/dependencies/__init__.py,sha256=JZMzn2ngqCek8ujgIXPDvEje6UZTcaOZ8knk3Z2JQs4,207
|
|
5
|
+
ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py,sha256=3FhSQW-oJcLeP45szxdp5bDxVKMiMPrdBE3srHjjWhU,3640
|
|
6
6
|
ipulse_shared_core_ftredge/dependencies/auth_protected_router.py,sha256=em5D5tE7OkgZmuCtYCKuUAnIZCgRJhCF8Ye5QmtGWlk,1807
|
|
7
|
-
ipulse_shared_core_ftredge/dependencies/
|
|
7
|
+
ipulse_shared_core_ftredge/dependencies/authz_credit_extraction.py,sha256=0A6AQ8k08IrutpWoz7gVlK5TjETS9lGOzbQ9cS1CA6I,3008
|
|
8
|
+
ipulse_shared_core_ftredge/dependencies/authz_for_apis.py,sha256=ECGffFwpBafDDtirKAMRL2j6rgDv6yjCywvGvV39nTw,15020
|
|
8
9
|
ipulse_shared_core_ftredge/dependencies/firestore_client.py,sha256=VbTb121nsc9EZPd1RDEsHBLW5pIiVw6Wdo2JFL4afMg,714
|
|
9
10
|
ipulse_shared_core_ftredge/exceptions/__init__.py,sha256=Cb_RsIie4DbT_NLwFVwjw4riDKsNNRQEuAvHvYa-Zco,1038
|
|
10
11
|
ipulse_shared_core_ftredge/exceptions/base_exceptions.py,sha256=117YsiCbYLLBu_D0IffYFVSX2yh-pisALMtoSMwj6xI,5338
|
|
11
12
|
ipulse_shared_core_ftredge/exceptions/user_exceptions.py,sha256=I-nm21MKrUYEoybpRODeYNzc184HfgHvRZQm_xux4VY,6824
|
|
12
|
-
ipulse_shared_core_ftredge/models/__init__.py,sha256=
|
|
13
|
-
ipulse_shared_core_ftredge/models/base_api_response.py,sha256=
|
|
13
|
+
ipulse_shared_core_ftredge/models/__init__.py,sha256=jhaVbOzJiFUf24saV1-a4GeKUaPif3jqAnIr4wgiUVs,455
|
|
14
|
+
ipulse_shared_core_ftredge/models/base_api_response.py,sha256=g7f1jH3VkEyMuW-d_j3ex2zk8LO2EMlkYRJQqYic_KU,792
|
|
14
15
|
ipulse_shared_core_ftredge/models/base_data_model.py,sha256=GZ7KTT5FanHTgvmaHHTxawzAJtuixkbyb-SuL-mjWys,2193
|
|
16
|
+
ipulse_shared_core_ftredge/models/credit_api_response.py,sha256=gavsJeuIsQJmY1t8DPzRZP5da68DT6IknE_oAeoapTc,797
|
|
17
|
+
ipulse_shared_core_ftredge/models/custom_json_response.py,sha256=5WCqpb6YEzZQitXE9DdnPv3x-me_0iV_qmd6laeiPWA,1396
|
|
15
18
|
ipulse_shared_core_ftredge/models/catalog/__init__.py,sha256=9oKJ74_mTtmj-0iDnRBiPI8m8QJ2J9wvx4ZWaZw3zRk,208
|
|
16
19
|
ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py,sha256=WxKWzTmHJlvFQj6Kq69iWMoFkx_veiPhonFo8dUGzZw,9148
|
|
17
20
|
ipulse_shared_core_ftredge/models/catalog/usertype.py,sha256=E_qQCq7ytiFca6umaX_-_a6TuDh83YwSKtFKdeU4ErM,6584
|
|
18
21
|
ipulse_shared_core_ftredge/models/user/__init__.py,sha256=1BKuWCvTTy6C5_vPPc0K3obRdpadiOVBzkFvKwzJ2vQ,215
|
|
19
22
|
ipulse_shared_core_ftredge/models/user/user_permissions.py,sha256=3dK4idfS7tsIXzN-c5plcg3qO5GPMtxzUoGCeBfzCis,2281
|
|
20
|
-
ipulse_shared_core_ftredge/models/user/user_subscription.py,sha256=
|
|
23
|
+
ipulse_shared_core_ftredge/models/user/user_subscription.py,sha256=66JYG-6a25nMTT9w25ZgOkB--MIOBDrqgp56If_9NSI,13546
|
|
21
24
|
ipulse_shared_core_ftredge/models/user/userauth.py,sha256=GYtRxYsbxEh3eBink7qr_CjxWu5W46G3gUL9_2QLw6U,5724
|
|
22
25
|
ipulse_shared_core_ftredge/models/user/userprofile.py,sha256=7VbE4qiKpDxZsNTk-IJKA32QxW0JOo8KWPkj8h9J2-Y,6945
|
|
23
|
-
ipulse_shared_core_ftredge/models/user/userstatus.py,sha256=
|
|
26
|
+
ipulse_shared_core_ftredge/models/user/userstatus.py,sha256=w6eWKfzZ1RP6u9wMrucXUV6XZpNcS-z8zEl8JNUt-o8,18798
|
|
24
27
|
ipulse_shared_core_ftredge/monitoring/__init__.py,sha256=gUoJjT0wj-cQYnMWheWbh1mmRHmaeojmnBZTj7KPNus,61
|
|
25
28
|
ipulse_shared_core_ftredge/monitoring/tracemon.py,sha256=Trku0qrwWvEcvKsBWiYokd_G3fcH-5uP2wRVgcgIz_k,11596
|
|
26
29
|
ipulse_shared_core_ftredge/services/__init__.py,sha256=9AkMLCHNswhuNbQuJZaEVz4zt4F84PxfJLyU_bYk4Js,565
|
|
27
|
-
ipulse_shared_core_ftredge/services/charging_processors.py,sha256=
|
|
28
|
-
ipulse_shared_core_ftredge/services/user_charging_service.py,sha256=
|
|
30
|
+
ipulse_shared_core_ftredge/services/charging_processors.py,sha256=Ynt8nQZprdsny9FaCB1ifrEIXPP6V7r_MxWID1BrRug,16391
|
|
31
|
+
ipulse_shared_core_ftredge/services/user_charging_service.py,sha256=WNVGn77Rcy_Al3M9wF0VZPYQVWL__E2baVRDQeA37I4,14609
|
|
29
32
|
ipulse_shared_core_ftredge/services/base/__init__.py,sha256=zhyrHQMM0cLJr4spk2b6VsgJXuWBy7hUBzhrq_Seg9k,389
|
|
30
33
|
ipulse_shared_core_ftredge/services/base/base_firestore_service.py,sha256=leZFwxb1ruheypqudpKnuNtRQXtO4KNeoJk6ZACozHc,19512
|
|
31
34
|
ipulse_shared_core_ftredge/services/base/cache_aware_firestore_service.py,sha256=ya5Asff9BQodYnJVAw6M_Pm8WtVRPpEK7izFlZ2MyjA,10016
|
|
32
35
|
ipulse_shared_core_ftredge/services/catalog/__init__.py,sha256=ctc2nDGwsW_Ji4lk9pys3oyNwR_V-gHSbSHawym5fKQ,385
|
|
33
36
|
ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py,sha256=X5xAi9sOk_F1ky0ECwPVlwIPPsN2PrZC6bN_pASGDjQ,9702
|
|
34
37
|
ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py,sha256=C_VWxZ5iPcybjsSXdmZHyqS--rI3KY8pp7JDIy_L7S8,12833
|
|
35
|
-
ipulse_shared_core_ftredge/services/user/__init__.py,sha256=
|
|
36
|
-
ipulse_shared_core_ftredge/services/user/
|
|
37
|
-
ipulse_shared_core_ftredge/services/user/
|
|
38
|
+
ipulse_shared_core_ftredge/services/user/__init__.py,sha256=PDpxTGt6Gn3yJNiU2vvksfJF78-wPsnA5yeTL9SeR1Y,945
|
|
39
|
+
ipulse_shared_core_ftredge/services/user/user_charging_operations.py,sha256=s4NKV7z35B2bnpmX4-XMWVAL9751xTW-sxIMjBnOvj8,32651
|
|
40
|
+
ipulse_shared_core_ftredge/services/user/user_core_service.py,sha256=M9H8dU9O21GVVXV400-sLUTPFXWzd-XODXChI59JXmM,32340
|
|
41
|
+
ipulse_shared_core_ftredge/services/user/user_multistep_operations.py,sha256=99oBqtLDb7TMTxWzTNjkjt3051oULbNPvyUwa9R-J8k,39604
|
|
38
42
|
ipulse_shared_core_ftredge/services/user/user_permissions_operations.py,sha256=FByszIWo-qooLVXFTw0tGLWksIJEqHUPc_ZGwue0_pM,15753
|
|
39
|
-
ipulse_shared_core_ftredge/services/user/user_subscription_operations.py,sha256=
|
|
43
|
+
ipulse_shared_core_ftredge/services/user/user_subscription_operations.py,sha256=P-Sif2tXXC6ifKZCEpkOLXP4kf1-kKH0tfo13XcRIqk,21655
|
|
40
44
|
ipulse_shared_core_ftredge/services/user/userauth_operations.py,sha256=QY_ueDei4EeXl_EZLlhN13PF6k_qsPjlnP0dnv2m7KI,36246
|
|
41
45
|
ipulse_shared_core_ftredge/services/user/userprofile_operations.py,sha256=_qyIEAQYCTV-subgP-5naMs_26apCpauomE6qmCCVWs,7333
|
|
42
|
-
ipulse_shared_core_ftredge/services/user/userstatus_operations.py,sha256=
|
|
46
|
+
ipulse_shared_core_ftredge/services/user/userstatus_operations.py,sha256=Om9d94cM4uOdTmCNXECU0hFtsUZ-o-OLMEiLAG74YUQ,22730
|
|
43
47
|
ipulse_shared_core_ftredge/utils/__init__.py,sha256=JnxUb8I2MRjJC7rBPXSrpwBIQDEOku5O9JsiTi3oun8,56
|
|
44
48
|
ipulse_shared_core_ftredge/utils/custom_json_encoder.py,sha256=DblQLD0KOSNDyQ58wQRogBrShIXzPIZUw_oGOBATnJY,1366
|
|
45
49
|
ipulse_shared_core_ftredge/utils/json_encoder.py,sha256=QkcaFneVv3-q-s__Dz4OiUWYnM6jgHDJrDMdPv09RCA,2093
|
|
46
|
-
ipulse_shared_core_ftredge-
|
|
47
|
-
ipulse_shared_core_ftredge-
|
|
48
|
-
ipulse_shared_core_ftredge-
|
|
49
|
-
ipulse_shared_core_ftredge-
|
|
50
|
-
ipulse_shared_core_ftredge-
|
|
50
|
+
ipulse_shared_core_ftredge-26.1.1.dist-info/licenses/LICENCE,sha256=YBtYAXNqCCOo9Mr2hfkbSPAM9CeAr2j1VZBSwQTrNwE,1060
|
|
51
|
+
ipulse_shared_core_ftredge-26.1.1.dist-info/METADATA,sha256=D2N24LBqNqge-0-H9nr3EldAmrTST9745Cyt4ITaKj0,782
|
|
52
|
+
ipulse_shared_core_ftredge-26.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
53
|
+
ipulse_shared_core_ftredge-26.1.1.dist-info/top_level.txt,sha256=8sgYrptpexkA_6_HyGvho26cVFH9kmtGvaK8tHbsGHk,27
|
|
54
|
+
ipulse_shared_core_ftredge-26.1.1.dist-info/RECORD,,
|
{ipulse_shared_core_ftredge-24.2.1.dist-info → ipulse_shared_core_ftredge-26.1.1.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|