ipulse-shared-core-ftredge 25.1.1__py3-none-any.whl → 27.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.
Files changed (20) hide show
  1. ipulse_shared_core_ftredge/dependencies/__init__.py +3 -1
  2. ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +0 -1
  3. ipulse_shared_core_ftredge/dependencies/authz_credit_extraction.py +67 -0
  4. ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +27 -19
  5. ipulse_shared_core_ftredge/models/__init__.py +3 -1
  6. ipulse_shared_core_ftredge/models/base_api_response.py +6 -43
  7. ipulse_shared_core_ftredge/models/credit_api_response.py +26 -0
  8. ipulse_shared_core_ftredge/models/custom_json_response.py +32 -0
  9. ipulse_shared_core_ftredge/services/charging_processors.py +13 -13
  10. ipulse_shared_core_ftredge/services/user/__init__.py +1 -0
  11. ipulse_shared_core_ftredge/services/user/user_charging_operations.py +721 -0
  12. ipulse_shared_core_ftredge/services/user/user_core_service.py +123 -20
  13. ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +42 -52
  14. ipulse_shared_core_ftredge/services/user_charging_service.py +18 -18
  15. ipulse_shared_core_ftredge/utils/authz_credit_extraction.py +0 -0
  16. {ipulse_shared_core_ftredge-25.1.1.dist-info → ipulse_shared_core_ftredge-27.1.1.dist-info}/METADATA +1 -1
  17. {ipulse_shared_core_ftredge-25.1.1.dist-info → ipulse_shared_core_ftredge-27.1.1.dist-info}/RECORD +20 -15
  18. {ipulse_shared_core_ftredge-25.1.1.dist-info → ipulse_shared_core_ftredge-27.1.1.dist-info}/WHEEL +0 -0
  19. {ipulse_shared_core_ftredge-25.1.1.dist-info → ipulse_shared_core_ftredge-27.1.1.dist-info}/licenses/LICENCE +0 -0
  20. {ipulse_shared_core_ftredge-25.1.1.dist-info → ipulse_shared_core_ftredge-27.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
- subscription_plans_collection: Collection name for subscription plans
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.iam_ops = UserpermissionsOperations(
91
+ self.usepermission_ops = UserpermissionsOperations(
91
92
  userstatus_ops=self.userstatus_ops,
92
93
  logger=self.logger
93
94
  )
94
95
 
95
- self.subscription_ops = UsersubscriptionOperations(
96
+ self.user_subscription_ops = UsersubscriptionOperations(
96
97
  firestore_client=self.db,
97
98
  userstatus_ops=self.userstatus_ops,
98
- permissions_ops=self.iam_ops,
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.subscription_ops,
120
- useriam_ops=self.iam_ops,
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.subscription_ops.fetch_subscriptionplan_and_apply_subscription_to_user(
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.subscription_ops.apply_subscriptionplan(
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.subscription_ops.cancel_user_subscription(user_uid=user_uid, updater_uid=updater_uid)
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.subscription_ops.get_user_active_subscription(user_uid=user_uid)
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.subscription_ops.update_user_subscription(user_uid=user_uid, subscription_updates=subscription_data, updater_uid=updater_uid)
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.subscription_ops.downgrade_user_subscription_to_fallback_subscriptionplan(
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.iam_ops.add_permission_to_user(user_uid=user_uid, permission=permission, updater_uid=updater_uid)
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.iam_ops.get_permissions_of_user(user_uid=user_uid)
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.iam_ops.remove_all_permissions_from_user(user_uid=user_uid, source=source, updater_uid=updater_uid)
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.iam_ops.remove_permission_from_user(
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.iam_ops.cleanup_expired_permissions_of_user(user_uid=user_uid, iam_unit_type=iam_unit_type, updater_uid=updater_uid)
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.iam_ops.get_bulk_users_with_permission(
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 in database (2 operations only)
215
- self.logger.info("Creating UserProfile for user: %s", user_uid)
216
- await self.userprofile_ops.create_userprofile(final_userprofile)
217
- profile_created = True
218
-
219
- self.logger.info("Creating UserStatus for user: %s (with %d IAM permissions)",
220
- user_uid, len(final_userstatus.iam_permissions))
221
- await self.userstatus_ops.create_userstatus(final_userstatus)
222
- status_created = True
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
- error_context = f"User creation from models failed: {e}"
236
- # Use firebase_user_uid if available, otherwise fall back to the original user_uid
237
- cleanup_uid = firebase_user_uid or userprofile.user_uid
238
- await self._rollback_user_creation(cleanup_uid, profile_created, status_created, error_context)
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(
@@ -41,7 +41,7 @@ class UserChargingService:
41
41
  f"Doc Prefix: {self.userstatus_doc_prefix}, Timeout: {self.timeout}s"
42
42
  )
43
43
 
44
- async def verify_credits(
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"verify_credits called for user {user_uid}, "
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={"credits_to_charge": required_credits_for_resource}
138
+ additional_info={"credits_to_take": required_credits_for_resource}
139
139
  ) from e
140
140
 
141
- async def charge_credits_transaction(self, user_uid: str, credits_to_charge: Optional[float], operation_details: str) -> Tuple[bool, Optional[Dict[str, float]]]:
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
- credits_to_charge: The number of credits to charge.
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 credits_to_charge is None (pricing not properly configured).
156
+ ValidationError: If credits_to_take is None (pricing not properly configured).
157
157
  """
158
- if credits_to_charge is None:
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 credits_to_charge == 0:
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 < credits_to_charge:
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 {credits_to_charge}"
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, credits_to_charge)
210
- remaining_charge = credits_to_charge - subscription_credits_deducted
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) < credits_to_charge:
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: {credits_to_charge}, Calculated deduction: {subscription_credits_deducted + extra_credits_deducted}"
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": "charging_service__charge_credits_transaction" # Static identifier for this operation
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 {credits_to_charge} credits for user {user_uid}. "
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 {credits_to_charge} credits for user {user_uid} (transaction outcome). "
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={"credits_to_charge": credits_to_charge}
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]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipulse_shared_core_ftredge
3
- Version: 25.1.1
3
+ Version: 27.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
@@ -1,17 +1,20 @@
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=0QEG_lQjtnxErpEDRHN8Q7Qz5dE8brzhFnmbIHVyoak,66
5
- ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py,sha256=3fHkjpcvywbWy_86iO_O1W75NiuTYMtP00LpA4ue-QU,3641
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/authz_for_apis.py,sha256=C424mYk2jtmkzC8su4uRJU_svI-vSRgX4j2DaYYNJx4,14962
7
+ ipulse_shared_core_ftredge/dependencies/authz_credit_extraction.py,sha256=0A6AQ8k08IrutpWoz7gVlK5TjETS9lGOzbQ9cS1CA6I,3008
8
+ ipulse_shared_core_ftredge/dependencies/authz_for_apis.py,sha256=_U3xcfOE-BcEbnU5MvxfqrcX_5I52SSjNEil52_xK3E,15431
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=00qXzkURwj3n0lmHXpcduQf9fUQO0rv0JOoavtoUfgE,369
13
- ipulse_shared_core_ftredge/models/base_api_response.py,sha256=OwuWI2PsMSLDkFt643u35ZhW5AHFEMMAGnGprmUO0fA,2380
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
@@ -24,27 +27,29 @@ ipulse_shared_core_ftredge/models/user/userstatus.py,sha256=w6eWKfzZ1RP6u9wMrucX
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=fug6Hr5JbkhV3TXNyHrN0U3hOYmD1frCpSXVu6CynXQ,16316
28
- ipulse_shared_core_ftredge/services/user_charging_service.py,sha256=wlQDURVRWAv8g_StQ9Ybbn2G_OWw6gYD47yWWk9Bd9A,14629
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=jmkD5XzAmaD8QV2UsgB5xynGcfsXliWtRtN2pt6kzbA,884
36
- ipulse_shared_core_ftredge/services/user/user_core_service.py,sha256=rJF4ZDk1ANz-gk7wEEXBbgjaqS4r1Vdo8yC00fQDytQ,28324
37
- ipulse_shared_core_ftredge/services/user/user_multistep_operations.py,sha256=UlSNs_8ORuESfsxHljIdcaD6H8DDEXSU0CzJAr99T5M,39765
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
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
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
48
+ ipulse_shared_core_ftredge/utils/authz_credit_extraction.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
49
  ipulse_shared_core_ftredge/utils/custom_json_encoder.py,sha256=DblQLD0KOSNDyQ58wQRogBrShIXzPIZUw_oGOBATnJY,1366
45
50
  ipulse_shared_core_ftredge/utils/json_encoder.py,sha256=QkcaFneVv3-q-s__Dz4OiUWYnM6jgHDJrDMdPv09RCA,2093
46
- ipulse_shared_core_ftredge-25.1.1.dist-info/licenses/LICENCE,sha256=YBtYAXNqCCOo9Mr2hfkbSPAM9CeAr2j1VZBSwQTrNwE,1060
47
- ipulse_shared_core_ftredge-25.1.1.dist-info/METADATA,sha256=RQwjz526aJVLHzF5_ggRv_8WK2cJfpzEVkIs71E7hpk,782
48
- ipulse_shared_core_ftredge-25.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
49
- ipulse_shared_core_ftredge-25.1.1.dist-info/top_level.txt,sha256=8sgYrptpexkA_6_HyGvho26cVFH9kmtGvaK8tHbsGHk,27
50
- ipulse_shared_core_ftredge-25.1.1.dist-info/RECORD,,
51
+ ipulse_shared_core_ftredge-27.1.1.dist-info/licenses/LICENCE,sha256=YBtYAXNqCCOo9Mr2hfkbSPAM9CeAr2j1VZBSwQTrNwE,1060
52
+ ipulse_shared_core_ftredge-27.1.1.dist-info/METADATA,sha256=A5SUuwijYQj_iImx4GN10U4bX2wg01rtLcgs11HM8iM,782
53
+ ipulse_shared_core_ftredge-27.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
54
+ ipulse_shared_core_ftredge-27.1.1.dist-info/top_level.txt,sha256=8sgYrptpexkA_6_HyGvho26cVFH9kmtGvaK8tHbsGHk,27
55
+ ipulse_shared_core_ftredge-27.1.1.dist-info/RECORD,,