ai-lls-lib 1.4.0rc3__py3-none-any.whl → 1.4.0rc4__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 (34) hide show
  1. ai_lls_lib/__init__.py +1 -1
  2. ai_lls_lib/auth/__init__.py +4 -4
  3. ai_lls_lib/auth/context_parser.py +68 -68
  4. ai_lls_lib/cli/__init__.py +3 -3
  5. ai_lls_lib/cli/__main__.py +30 -30
  6. ai_lls_lib/cli/aws_client.py +115 -115
  7. ai_lls_lib/cli/commands/__init__.py +3 -3
  8. ai_lls_lib/cli/commands/admin.py +174 -174
  9. ai_lls_lib/cli/commands/cache.py +142 -142
  10. ai_lls_lib/cli/commands/stripe.py +377 -377
  11. ai_lls_lib/cli/commands/test_stack.py +216 -216
  12. ai_lls_lib/cli/commands/verify.py +111 -111
  13. ai_lls_lib/cli/env_loader.py +122 -122
  14. ai_lls_lib/core/__init__.py +3 -3
  15. ai_lls_lib/core/cache.py +106 -106
  16. ai_lls_lib/core/models.py +77 -77
  17. ai_lls_lib/core/processor.py +295 -295
  18. ai_lls_lib/core/verifier.py +84 -84
  19. ai_lls_lib/payment/__init__.py +13 -13
  20. ai_lls_lib/payment/credit_manager.py +186 -186
  21. ai_lls_lib/payment/models.py +102 -102
  22. ai_lls_lib/payment/stripe_manager.py +487 -487
  23. ai_lls_lib/payment/webhook_processor.py +215 -215
  24. ai_lls_lib/providers/__init__.py +7 -7
  25. ai_lls_lib/providers/base.py +28 -28
  26. ai_lls_lib/providers/external.py +87 -87
  27. ai_lls_lib/providers/stub.py +48 -48
  28. ai_lls_lib/testing/__init__.py +3 -3
  29. ai_lls_lib/testing/fixtures.py +104 -104
  30. {ai_lls_lib-1.4.0rc3.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/METADATA +1 -1
  31. ai_lls_lib-1.4.0rc4.dist-info/RECORD +33 -0
  32. ai_lls_lib-1.4.0rc3.dist-info/RECORD +0 -33
  33. {ai_lls_lib-1.4.0rc3.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/WHEEL +0 -0
  34. {ai_lls_lib-1.4.0rc3.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/entry_points.txt +0 -0
@@ -1,215 +1,215 @@
1
- """Stripe webhook event processing."""
2
-
3
- import json
4
- import logging
5
- from typing import Dict, Any
6
-
7
- try:
8
- import stripe
9
- except ImportError:
10
- stripe = None
11
-
12
- from .credit_manager import CreditManager
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
-
17
- class WebhookProcessor:
18
- """Process Stripe webhook events."""
19
-
20
- def __init__(self, webhook_secret: str, credit_manager: CreditManager):
21
- """Initialize with webhook secret and credit manager."""
22
- self.webhook_secret = webhook_secret
23
- self.credit_manager = credit_manager
24
-
25
- def verify_and_parse(self, payload: str, signature: str) -> Dict[str, Any]:
26
- """Verify webhook signature and parse event."""
27
- if not stripe:
28
- raise ImportError("stripe package not installed")
29
-
30
- try:
31
- event = stripe.Webhook.construct_event(
32
- payload, signature, self.webhook_secret
33
- )
34
- return event
35
- except ValueError as e:
36
- logger.error(f"Invalid webhook payload: {e}")
37
- raise
38
- except stripe.error.SignatureVerificationError as e:
39
- logger.error(f"Invalid webhook signature: {e}")
40
- raise
41
-
42
- def process_event(self, event: Dict[str, Any]) -> Dict[str, Any]:
43
- """
44
- Process a verified webhook event.
45
- Returns response data.
46
- """
47
- event_type = event.get("type")
48
- event_data = event.get("data", {}).get("object", {})
49
-
50
- logger.info(f"Processing webhook event: {event_type}")
51
-
52
- if event_type == "payment_intent.succeeded":
53
- return self._handle_payment_intent_succeeded(event_data)
54
-
55
- elif event_type == "checkout.session.completed":
56
- return self._handle_checkout_completed(event_data)
57
-
58
- elif event_type == "customer.subscription.created":
59
- return self._handle_subscription_created(event_data)
60
-
61
- elif event_type == "customer.subscription.updated":
62
- return self._handle_subscription_updated(event_data)
63
-
64
- elif event_type == "customer.subscription.deleted":
65
- return self._handle_subscription_deleted(event_data)
66
-
67
- elif event_type == "invoice.payment_succeeded":
68
- return self._handle_invoice_paid(event_data)
69
-
70
- elif event_type == "invoice.payment_failed":
71
- return self._handle_invoice_failed(event_data)
72
-
73
- elif event_type == "charge.dispute.created":
74
- return self._handle_dispute_created(event_data)
75
-
76
- else:
77
- logger.info(f"Unhandled event type: {event_type}")
78
- return {"message": f"Event {event_type} received but not processed"}
79
-
80
- def _handle_checkout_completed(self, session: Dict[str, Any]) -> Dict[str, Any]:
81
- """Handle successful checkout session for credit purchase."""
82
- metadata = session.get("metadata", {})
83
- user_id = metadata.get("user_id")
84
-
85
- if not user_id:
86
- logger.error("No user_id in checkout session metadata")
87
- return {"error": "Missing user_id"}
88
-
89
- # Get line items to determine credits purchased
90
- if session.get("mode") == "payment":
91
- # One-time payment for credits
92
- # In production, fetch line items from Stripe to get price metadata
93
- # For now, extract from session metadata if available
94
- credits = int(metadata.get("credits", 0))
95
-
96
- if credits > 0:
97
- new_balance = self.credit_manager.add_credits(user_id, credits)
98
- logger.info(f"Added {credits} credits to user {user_id}, new balance: {new_balance}")
99
- return {"credits_added": credits, "new_balance": new_balance}
100
-
101
- return {"message": "Checkout processed"}
102
-
103
- def _handle_subscription_created(self, subscription: Dict[str, Any]) -> Dict[str, Any]:
104
- """Handle new subscription creation."""
105
- metadata = subscription.get("metadata", {})
106
- user_id = metadata.get("user_id")
107
- customer_id = subscription.get("customer")
108
- subscription_id = subscription.get("id")
109
- status = subscription.get("status")
110
-
111
- if user_id:
112
- self.credit_manager.set_subscription_state(
113
- user_id=user_id,
114
- status=status,
115
- stripe_customer_id=customer_id,
116
- stripe_subscription_id=subscription_id
117
- )
118
- logger.info(f"Created subscription {subscription_id} for user {user_id}")
119
-
120
- return {"subscription_id": subscription_id, "status": status}
121
-
122
- def _handle_subscription_updated(self, subscription: Dict[str, Any]) -> Dict[str, Any]:
123
- """Handle subscription updates (pause/resume/etc)."""
124
- metadata = subscription.get("metadata", {})
125
- user_id = metadata.get("user_id")
126
- subscription_id = subscription.get("id")
127
- status = subscription.get("status")
128
-
129
- if user_id:
130
- self.credit_manager.set_subscription_state(
131
- user_id=user_id,
132
- status=status,
133
- stripe_subscription_id=subscription_id
134
- )
135
- logger.info(f"Updated subscription {subscription_id} status to {status}")
136
-
137
- return {"subscription_id": subscription_id, "status": status}
138
-
139
- def _handle_subscription_deleted(self, subscription: Dict[str, Any]) -> Dict[str, Any]:
140
- """Handle subscription cancellation."""
141
- metadata = subscription.get("metadata", {})
142
- user_id = metadata.get("user_id")
143
- subscription_id = subscription.get("id")
144
-
145
- if user_id:
146
- self.credit_manager.set_subscription_state(
147
- user_id=user_id,
148
- status="cancelled",
149
- stripe_subscription_id=subscription_id
150
- )
151
- logger.info(f"Cancelled subscription {subscription_id} for user {user_id}")
152
-
153
- return {"subscription_id": subscription_id, "status": "cancelled"}
154
-
155
- def _handle_invoice_paid(self, invoice: Dict[str, Any]) -> Dict[str, Any]:
156
- """Handle successful subscription payment."""
157
- # For monthly subscriptions, could grant monthly credit allotment here
158
- # For now, just log the payment
159
- customer_id = invoice.get("customer")
160
- amount = invoice.get("amount_paid", 0) / 100.0
161
- logger.info(f"Invoice paid: ${amount} from customer {customer_id}")
162
- return {"amount_paid": amount}
163
-
164
- def _handle_invoice_failed(self, invoice: Dict[str, Any]) -> Dict[str, Any]:
165
- """Handle failed subscription payment."""
166
- customer_id = invoice.get("customer")
167
- logger.warning(f"Invoice payment failed for customer {customer_id}")
168
- # Could pause subscription or send notification here
169
- return {"status": "payment_failed"}
170
-
171
- def _handle_payment_intent_succeeded(self, payment_intent: Dict[str, Any]) -> Dict[str, Any]:
172
- """Handle successful payment intent (credit purchase)."""
173
- metadata = payment_intent.get("metadata", {})
174
- user_id = metadata.get("user_id")
175
-
176
- if not user_id:
177
- logger.error("No user_id in payment_intent metadata")
178
- return {"error": "Missing user_id"}
179
-
180
- # Check if this is a verification charge ($1)
181
- if metadata.get("type") == "verification":
182
- # This was the $1 verification, credits already added in payment_setup handler
183
- logger.info(f"Verification charge completed for user {user_id}")
184
- return {"type": "verification", "status": "completed"}
185
-
186
- # Get credits from metadata (set during payment creation)
187
- credits = int(metadata.get("credits", 0))
188
-
189
- if credits > 0:
190
- new_balance = self.credit_manager.add_credits(user_id, credits)
191
- logger.info(f"Added {credits} credits to user {user_id}, new balance: {new_balance}")
192
- return {"credits_added": credits, "new_balance": new_balance}
193
-
194
- return {"message": "Payment processed"}
195
-
196
- def _handle_dispute_created(self, dispute: Dict[str, Any]) -> Dict[str, Any]:
197
- """Handle charge dispute (mark account as disputed)."""
198
- # Get the charge and its metadata
199
- charge_id = dispute.get("charge")
200
-
201
- if not charge_id:
202
- logger.error("No charge_id in dispute")
203
- return {"error": "Missing charge_id"}
204
-
205
- # In production, would fetch the charge from Stripe to get metadata
206
- # For now, log the dispute for manual handling
207
- amount = dispute.get("amount", 0) / 100.0
208
- reason = dispute.get("reason", "unknown")
209
-
210
- logger.warning(f"Dispute created for charge {charge_id}: ${amount}, reason: {reason}")
211
-
212
- # TODO: Mark user account as disputed in CreditsTable
213
- # This would prevent new purchases until resolved
214
-
215
- return {"dispute_id": dispute.get("id"), "status": "created", "amount": amount}
1
+ """Stripe webhook event processing."""
2
+
3
+ import json
4
+ import logging
5
+ from typing import Dict, Any
6
+
7
+ try:
8
+ import stripe
9
+ except ImportError:
10
+ stripe = None
11
+
12
+ from .credit_manager import CreditManager
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class WebhookProcessor:
18
+ """Process Stripe webhook events."""
19
+
20
+ def __init__(self, webhook_secret: str, credit_manager: CreditManager):
21
+ """Initialize with webhook secret and credit manager."""
22
+ self.webhook_secret = webhook_secret
23
+ self.credit_manager = credit_manager
24
+
25
+ def verify_and_parse(self, payload: str, signature: str) -> Dict[str, Any]:
26
+ """Verify webhook signature and parse event."""
27
+ if not stripe:
28
+ raise ImportError("stripe package not installed")
29
+
30
+ try:
31
+ event = stripe.Webhook.construct_event(
32
+ payload, signature, self.webhook_secret
33
+ )
34
+ return event
35
+ except ValueError as e:
36
+ logger.error(f"Invalid webhook payload: {e}")
37
+ raise
38
+ except stripe.error.SignatureVerificationError as e:
39
+ logger.error(f"Invalid webhook signature: {e}")
40
+ raise
41
+
42
+ def process_event(self, event: Dict[str, Any]) -> Dict[str, Any]:
43
+ """
44
+ Process a verified webhook event.
45
+ Returns response data.
46
+ """
47
+ event_type = event.get("type")
48
+ event_data = event.get("data", {}).get("object", {})
49
+
50
+ logger.info(f"Processing webhook event: {event_type}")
51
+
52
+ if event_type == "payment_intent.succeeded":
53
+ return self._handle_payment_intent_succeeded(event_data)
54
+
55
+ elif event_type == "checkout.session.completed":
56
+ return self._handle_checkout_completed(event_data)
57
+
58
+ elif event_type == "customer.subscription.created":
59
+ return self._handle_subscription_created(event_data)
60
+
61
+ elif event_type == "customer.subscription.updated":
62
+ return self._handle_subscription_updated(event_data)
63
+
64
+ elif event_type == "customer.subscription.deleted":
65
+ return self._handle_subscription_deleted(event_data)
66
+
67
+ elif event_type == "invoice.payment_succeeded":
68
+ return self._handle_invoice_paid(event_data)
69
+
70
+ elif event_type == "invoice.payment_failed":
71
+ return self._handle_invoice_failed(event_data)
72
+
73
+ elif event_type == "charge.dispute.created":
74
+ return self._handle_dispute_created(event_data)
75
+
76
+ else:
77
+ logger.info(f"Unhandled event type: {event_type}")
78
+ return {"message": f"Event {event_type} received but not processed"}
79
+
80
+ def _handle_checkout_completed(self, session: Dict[str, Any]) -> Dict[str, Any]:
81
+ """Handle successful checkout session for credit purchase."""
82
+ metadata = session.get("metadata", {})
83
+ user_id = metadata.get("user_id")
84
+
85
+ if not user_id:
86
+ logger.error("No user_id in checkout session metadata")
87
+ return {"error": "Missing user_id"}
88
+
89
+ # Get line items to determine credits purchased
90
+ if session.get("mode") == "payment":
91
+ # One-time payment for credits
92
+ # In production, fetch line items from Stripe to get price metadata
93
+ # For now, extract from session metadata if available
94
+ credits = int(metadata.get("credits", 0))
95
+
96
+ if credits > 0:
97
+ new_balance = self.credit_manager.add_credits(user_id, credits)
98
+ logger.info(f"Added {credits} credits to user {user_id}, new balance: {new_balance}")
99
+ return {"credits_added": credits, "new_balance": new_balance}
100
+
101
+ return {"message": "Checkout processed"}
102
+
103
+ def _handle_subscription_created(self, subscription: Dict[str, Any]) -> Dict[str, Any]:
104
+ """Handle new subscription creation."""
105
+ metadata = subscription.get("metadata", {})
106
+ user_id = metadata.get("user_id")
107
+ customer_id = subscription.get("customer")
108
+ subscription_id = subscription.get("id")
109
+ status = subscription.get("status")
110
+
111
+ if user_id:
112
+ self.credit_manager.set_subscription_state(
113
+ user_id=user_id,
114
+ status=status,
115
+ stripe_customer_id=customer_id,
116
+ stripe_subscription_id=subscription_id
117
+ )
118
+ logger.info(f"Created subscription {subscription_id} for user {user_id}")
119
+
120
+ return {"subscription_id": subscription_id, "status": status}
121
+
122
+ def _handle_subscription_updated(self, subscription: Dict[str, Any]) -> Dict[str, Any]:
123
+ """Handle subscription updates (pause/resume/etc)."""
124
+ metadata = subscription.get("metadata", {})
125
+ user_id = metadata.get("user_id")
126
+ subscription_id = subscription.get("id")
127
+ status = subscription.get("status")
128
+
129
+ if user_id:
130
+ self.credit_manager.set_subscription_state(
131
+ user_id=user_id,
132
+ status=status,
133
+ stripe_subscription_id=subscription_id
134
+ )
135
+ logger.info(f"Updated subscription {subscription_id} status to {status}")
136
+
137
+ return {"subscription_id": subscription_id, "status": status}
138
+
139
+ def _handle_subscription_deleted(self, subscription: Dict[str, Any]) -> Dict[str, Any]:
140
+ """Handle subscription cancellation."""
141
+ metadata = subscription.get("metadata", {})
142
+ user_id = metadata.get("user_id")
143
+ subscription_id = subscription.get("id")
144
+
145
+ if user_id:
146
+ self.credit_manager.set_subscription_state(
147
+ user_id=user_id,
148
+ status="cancelled",
149
+ stripe_subscription_id=subscription_id
150
+ )
151
+ logger.info(f"Cancelled subscription {subscription_id} for user {user_id}")
152
+
153
+ return {"subscription_id": subscription_id, "status": "cancelled"}
154
+
155
+ def _handle_invoice_paid(self, invoice: Dict[str, Any]) -> Dict[str, Any]:
156
+ """Handle successful subscription payment."""
157
+ # For monthly subscriptions, could grant monthly credit allotment here
158
+ # For now, just log the payment
159
+ customer_id = invoice.get("customer")
160
+ amount = invoice.get("amount_paid", 0) / 100.0
161
+ logger.info(f"Invoice paid: ${amount} from customer {customer_id}")
162
+ return {"amount_paid": amount}
163
+
164
+ def _handle_invoice_failed(self, invoice: Dict[str, Any]) -> Dict[str, Any]:
165
+ """Handle failed subscription payment."""
166
+ customer_id = invoice.get("customer")
167
+ logger.warning(f"Invoice payment failed for customer {customer_id}")
168
+ # Could pause subscription or send notification here
169
+ return {"status": "payment_failed"}
170
+
171
+ def _handle_payment_intent_succeeded(self, payment_intent: Dict[str, Any]) -> Dict[str, Any]:
172
+ """Handle successful payment intent (credit purchase)."""
173
+ metadata = payment_intent.get("metadata", {})
174
+ user_id = metadata.get("user_id")
175
+
176
+ if not user_id:
177
+ logger.error("No user_id in payment_intent metadata")
178
+ return {"error": "Missing user_id"}
179
+
180
+ # Check if this is a verification charge ($1)
181
+ if metadata.get("type") == "verification":
182
+ # This was the $1 verification, credits already added in payment_setup handler
183
+ logger.info(f"Verification charge completed for user {user_id}")
184
+ return {"type": "verification", "status": "completed"}
185
+
186
+ # Get credits from metadata (set during payment creation)
187
+ credits = int(metadata.get("credits", 0))
188
+
189
+ if credits > 0:
190
+ new_balance = self.credit_manager.add_credits(user_id, credits)
191
+ logger.info(f"Added {credits} credits to user {user_id}, new balance: {new_balance}")
192
+ return {"credits_added": credits, "new_balance": new_balance}
193
+
194
+ return {"message": "Payment processed"}
195
+
196
+ def _handle_dispute_created(self, dispute: Dict[str, Any]) -> Dict[str, Any]:
197
+ """Handle charge dispute (mark account as disputed)."""
198
+ # Get the charge and its metadata
199
+ charge_id = dispute.get("charge")
200
+
201
+ if not charge_id:
202
+ logger.error("No charge_id in dispute")
203
+ return {"error": "Missing charge_id"}
204
+
205
+ # In production, would fetch the charge from Stripe to get metadata
206
+ # For now, log the dispute for manual handling
207
+ amount = dispute.get("amount", 0) / 100.0
208
+ reason = dispute.get("reason", "unknown")
209
+
210
+ logger.warning(f"Dispute created for charge {charge_id}: ${amount}, reason: {reason}")
211
+
212
+ # TODO: Mark user account as disputed in CreditsTable
213
+ # This would prevent new purchases until resolved
214
+
215
+ return {"dispute_id": dispute.get("id"), "status": "created", "amount": amount}
@@ -1,7 +1,7 @@
1
- """
2
- Verification providers for phone number checking
3
- """
4
- from .base import VerificationProvider
5
- from .stub import StubProvider
6
-
7
- __all__ = ["VerificationProvider", "StubProvider"]
1
+ """
2
+ Verification providers for phone number checking
3
+ """
4
+ from .base import VerificationProvider
5
+ from .stub import StubProvider
6
+
7
+ __all__ = ["VerificationProvider", "StubProvider"]
@@ -1,28 +1,28 @@
1
- """
2
- Base protocol for verification providers
3
- """
4
- from typing import Protocol, Tuple
5
- from ..core.models import LineType
6
-
7
-
8
- class VerificationProvider(Protocol):
9
- """
10
- Protocol for phone verification providers.
11
- All providers must implement this interface.
12
- """
13
-
14
- def verify_phone(self, phone: str) -> Tuple[LineType, bool]:
15
- """
16
- Verify a phone number's line type and DNC status.
17
-
18
- Args:
19
- phone: E.164 formatted phone number
20
-
21
- Returns:
22
- Tuple of (line_type, is_on_dnc_list)
23
-
24
- Raises:
25
- ValueError: If phone format is invalid
26
- Exception: For provider-specific errors
27
- """
28
- ...
1
+ """
2
+ Base protocol for verification providers
3
+ """
4
+ from typing import Protocol, Tuple
5
+ from ..core.models import LineType
6
+
7
+
8
+ class VerificationProvider(Protocol):
9
+ """
10
+ Protocol for phone verification providers.
11
+ All providers must implement this interface.
12
+ """
13
+
14
+ def verify_phone(self, phone: str) -> Tuple[LineType, bool]:
15
+ """
16
+ Verify a phone number's line type and DNC status.
17
+
18
+ Args:
19
+ phone: E.164 formatted phone number
20
+
21
+ Returns:
22
+ Tuple of (line_type, is_on_dnc_list)
23
+
24
+ Raises:
25
+ ValueError: If phone format is invalid
26
+ Exception: For provider-specific errors
27
+ """
28
+ ...