django-cfg 1.2.21__py3-none-any.whl → 1.2.23__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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/newsletter/signals.py +9 -8
- django_cfg/apps/payments/__init__.py +8 -0
- django_cfg/apps/payments/admin/__init__.py +23 -0
- django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
- django_cfg/apps/payments/admin/balance_admin.py +434 -0
- django_cfg/apps/payments/admin/currencies_admin.py +186 -0
- django_cfg/apps/payments/admin/filters.py +259 -0
- django_cfg/apps/payments/admin/payments_admin.py +142 -0
- django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
- django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
- django_cfg/apps/payments/apps.py +22 -0
- django_cfg/apps/payments/config/__init__.py +87 -0
- django_cfg/apps/payments/config/module.py +162 -0
- django_cfg/apps/payments/config/providers.py +93 -0
- django_cfg/apps/payments/config/settings.py +136 -0
- django_cfg/apps/payments/config/utils.py +198 -0
- django_cfg/apps/payments/decorators.py +291 -0
- django_cfg/apps/payments/managers/__init__.py +22 -0
- django_cfg/apps/payments/managers/api_key_manager.py +35 -0
- django_cfg/apps/payments/managers/balance_manager.py +361 -0
- django_cfg/apps/payments/managers/currency_manager.py +32 -0
- django_cfg/apps/payments/managers/payment_manager.py +44 -0
- django_cfg/apps/payments/managers/subscription_manager.py +37 -0
- django_cfg/apps/payments/managers/tariff_manager.py +29 -0
- django_cfg/apps/payments/middleware/__init__.py +13 -0
- django_cfg/apps/payments/middleware/api_access.py +261 -0
- django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
- django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
- django_cfg/apps/payments/migrations/0001_initial.py +1003 -0
- django_cfg/apps/payments/migrations/__init__.py +1 -0
- django_cfg/apps/payments/models/__init__.py +67 -0
- django_cfg/apps/payments/models/api_keys.py +96 -0
- django_cfg/apps/payments/models/balance.py +209 -0
- django_cfg/apps/payments/models/base.py +30 -0
- django_cfg/apps/payments/models/currencies.py +138 -0
- django_cfg/apps/payments/models/events.py +73 -0
- django_cfg/apps/payments/models/payments.py +301 -0
- django_cfg/apps/payments/models/subscriptions.py +270 -0
- django_cfg/apps/payments/models/tariffs.py +102 -0
- django_cfg/apps/payments/serializers/__init__.py +56 -0
- django_cfg/apps/payments/serializers/api_keys.py +51 -0
- django_cfg/apps/payments/serializers/balance.py +59 -0
- django_cfg/apps/payments/serializers/currencies.py +55 -0
- django_cfg/apps/payments/serializers/payments.py +62 -0
- django_cfg/apps/payments/serializers/subscriptions.py +71 -0
- django_cfg/apps/payments/serializers/tariffs.py +56 -0
- django_cfg/apps/payments/services/__init__.py +65 -0
- django_cfg/apps/payments/services/billing/__init__.py +8 -0
- django_cfg/apps/payments/services/cache/__init__.py +15 -0
- django_cfg/apps/payments/services/cache/base.py +30 -0
- django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
- django_cfg/apps/payments/services/core/__init__.py +17 -0
- django_cfg/apps/payments/services/core/balance_service.py +449 -0
- django_cfg/apps/payments/services/core/payment_service.py +393 -0
- django_cfg/apps/payments/services/core/subscription_service.py +616 -0
- django_cfg/apps/payments/services/internal_types.py +266 -0
- django_cfg/apps/payments/services/middleware/__init__.py +8 -0
- django_cfg/apps/payments/services/providers/__init__.py +19 -0
- django_cfg/apps/payments/services/providers/base.py +137 -0
- django_cfg/apps/payments/services/providers/cryptapi.py +262 -0
- django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
- django_cfg/apps/payments/services/providers/registry.py +99 -0
- django_cfg/apps/payments/services/validators/__init__.py +8 -0
- django_cfg/apps/payments/signals/__init__.py +13 -0
- django_cfg/apps/payments/signals/api_key_signals.py +150 -0
- django_cfg/apps/payments/signals/payment_signals.py +127 -0
- django_cfg/apps/payments/signals/subscription_signals.py +196 -0
- django_cfg/apps/payments/urls.py +78 -0
- django_cfg/apps/payments/utils/__init__.py +42 -0
- django_cfg/apps/payments/utils/config_utils.py +243 -0
- django_cfg/apps/payments/utils/middleware_utils.py +228 -0
- django_cfg/apps/payments/utils/validation_utils.py +94 -0
- django_cfg/apps/payments/views/__init__.py +62 -0
- django_cfg/apps/payments/views/api_key_views.py +164 -0
- django_cfg/apps/payments/views/balance_views.py +75 -0
- django_cfg/apps/payments/views/currency_views.py +111 -0
- django_cfg/apps/payments/views/payment_views.py +111 -0
- django_cfg/apps/payments/views/subscription_views.py +135 -0
- django_cfg/apps/payments/views/tariff_views.py +131 -0
- django_cfg/apps/support/signals.py +16 -4
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/core/config.py +6 -0
- django_cfg/models/revolution.py +14 -0
- django_cfg/modules/base.py +9 -0
- django_cfg/modules/django_email.py +42 -4
- django_cfg/modules/django_unfold/dashboard.py +20 -0
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/METADATA +2 -1
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/RECORD +92 -14
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,293 @@
|
|
1
|
+
"""
|
2
|
+
NowPayments provider implementation.
|
3
|
+
|
4
|
+
Enhanced crypto payment provider with minimal typing.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
import requests
|
9
|
+
import hashlib
|
10
|
+
import hmac
|
11
|
+
from typing import Optional, List
|
12
|
+
from decimal import Decimal
|
13
|
+
from pydantic import BaseModel, Field
|
14
|
+
|
15
|
+
from .base import PaymentProvider
|
16
|
+
from ..internal_types import ProviderResponse, WebhookData
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
class NowPaymentsConfig(BaseModel):
|
22
|
+
"""NowPayments provider configuration."""
|
23
|
+
api_key: str = Field(..., description="NowPayments API key")
|
24
|
+
sandbox: bool = Field(default=False, description="Use sandbox mode")
|
25
|
+
ipn_secret: Optional[str] = Field(default=None, description="IPN secret for webhook validation")
|
26
|
+
callback_url: Optional[str] = Field(default=None, description="Webhook callback URL")
|
27
|
+
success_url: Optional[str] = Field(default=None, description="Payment success redirect URL")
|
28
|
+
cancel_url: Optional[str] = Field(default=None, description="Payment cancel redirect URL")
|
29
|
+
enabled: bool = Field(default=True, description="Provider enabled")
|
30
|
+
|
31
|
+
|
32
|
+
class NowPaymentsProvider(PaymentProvider):
|
33
|
+
"""NowPayments cryptocurrency payment provider."""
|
34
|
+
|
35
|
+
def __init__(self, config: NowPaymentsConfig):
|
36
|
+
"""Initialize NowPayments provider."""
|
37
|
+
super().__init__(config.dict())
|
38
|
+
self.config = config
|
39
|
+
self.api_key = config.api_key
|
40
|
+
self.sandbox = config.sandbox
|
41
|
+
self.ipn_secret = config.ipn_secret or ''
|
42
|
+
self.base_url = self._get_base_url()
|
43
|
+
|
44
|
+
# Configurable URLs
|
45
|
+
self.callback_url = config.callback_url
|
46
|
+
self.success_url = config.success_url
|
47
|
+
self.cancel_url = config.cancel_url
|
48
|
+
|
49
|
+
self.headers = {
|
50
|
+
'x-api-key': self.api_key,
|
51
|
+
'Content-Type': 'application/json'
|
52
|
+
}
|
53
|
+
|
54
|
+
def _get_base_url(self) -> str:
|
55
|
+
"""Get base URL based on sandbox mode."""
|
56
|
+
if self.sandbox:
|
57
|
+
return 'https://api-sandbox.nowpayments.io/v1'
|
58
|
+
return 'https://api.nowpayments.io/v1'
|
59
|
+
|
60
|
+
def _make_request(self, method: str, endpoint: str, data: Optional[dict] = None) -> Optional[dict]:
|
61
|
+
"""Make HTTP request to NowPayments API with error handling."""
|
62
|
+
try:
|
63
|
+
url = f"{self.base_url}/{endpoint}"
|
64
|
+
|
65
|
+
response = requests.request(
|
66
|
+
method=method,
|
67
|
+
url=url,
|
68
|
+
headers=self.headers,
|
69
|
+
json=data,
|
70
|
+
timeout=30
|
71
|
+
)
|
72
|
+
|
73
|
+
response.raise_for_status()
|
74
|
+
return response.json()
|
75
|
+
|
76
|
+
except requests.exceptions.RequestException as e:
|
77
|
+
logger.error(f"NowPayments API request failed: {e}")
|
78
|
+
return None
|
79
|
+
except Exception as e:
|
80
|
+
logger.error(f"Unexpected error in NowPayments request: {e}")
|
81
|
+
return None
|
82
|
+
|
83
|
+
def create_payment(self, payment_data: dict) -> ProviderResponse:
|
84
|
+
"""Create payment via NowPayments API."""
|
85
|
+
try:
|
86
|
+
amount = Decimal(str(payment_data['amount']))
|
87
|
+
currency = payment_data['currency']
|
88
|
+
order_id = payment_data.get('order_id', f'payment_{int(amount * 100)}_{currency}')
|
89
|
+
|
90
|
+
payment_request = {
|
91
|
+
'price_amount': float(amount),
|
92
|
+
'price_currency': 'usd', # Base currency
|
93
|
+
'pay_currency': currency,
|
94
|
+
'order_id': order_id,
|
95
|
+
'order_description': payment_data.get('description', f'Payment {order_id}'),
|
96
|
+
}
|
97
|
+
|
98
|
+
# Add optional URLs
|
99
|
+
if self.success_url:
|
100
|
+
payment_request['success_url'] = self.success_url
|
101
|
+
if self.cancel_url:
|
102
|
+
payment_request['cancel_url'] = self.cancel_url
|
103
|
+
if self.callback_url:
|
104
|
+
payment_request['ipn_callback_url'] = self.callback_url
|
105
|
+
|
106
|
+
response = self._make_request('POST', 'payment', payment_request)
|
107
|
+
|
108
|
+
if response:
|
109
|
+
return ProviderResponse(
|
110
|
+
success=True,
|
111
|
+
provider_payment_id=response.get('payment_id'),
|
112
|
+
payment_url=response.get('invoice_url'),
|
113
|
+
pay_address=response.get('pay_address'),
|
114
|
+
amount=Decimal(str(response.get('pay_amount', 0))),
|
115
|
+
currency=response.get('pay_currency'),
|
116
|
+
status='pending'
|
117
|
+
)
|
118
|
+
else:
|
119
|
+
return ProviderResponse(
|
120
|
+
success=False,
|
121
|
+
error_message='Failed to create payment'
|
122
|
+
)
|
123
|
+
|
124
|
+
except Exception as e:
|
125
|
+
logger.error(f"NowPayments create_payment error: {e}")
|
126
|
+
return ProviderResponse(
|
127
|
+
success=False,
|
128
|
+
error_message=str(e)
|
129
|
+
)
|
130
|
+
|
131
|
+
def check_payment_status(self, payment_id: str) -> ProviderResponse:
|
132
|
+
"""Check payment status via NowPayments API."""
|
133
|
+
try:
|
134
|
+
response = self._make_request('GET', f'payment/{payment_id}')
|
135
|
+
|
136
|
+
if response:
|
137
|
+
# Map NowPayments status to universal status
|
138
|
+
status_mapping = {
|
139
|
+
'waiting': 'pending',
|
140
|
+
'confirming': 'processing',
|
141
|
+
'confirmed': 'completed',
|
142
|
+
'sending': 'processing',
|
143
|
+
'partially_paid': 'pending',
|
144
|
+
'finished': 'completed',
|
145
|
+
'failed': 'failed',
|
146
|
+
'refunded': 'refunded',
|
147
|
+
'expired': 'expired'
|
148
|
+
}
|
149
|
+
|
150
|
+
provider_status = response.get('payment_status', 'unknown')
|
151
|
+
universal_status = status_mapping.get(provider_status, 'unknown')
|
152
|
+
|
153
|
+
return ProviderResponse(
|
154
|
+
success=True,
|
155
|
+
provider_payment_id=response.get('payment_id'),
|
156
|
+
status=universal_status,
|
157
|
+
pay_address=response.get('pay_address'),
|
158
|
+
amount=Decimal(str(response.get('pay_amount', 0))),
|
159
|
+
currency=response.get('pay_currency')
|
160
|
+
)
|
161
|
+
else:
|
162
|
+
return ProviderResponse(
|
163
|
+
success=False,
|
164
|
+
error_message='Payment not found'
|
165
|
+
)
|
166
|
+
|
167
|
+
except Exception as e:
|
168
|
+
logger.error(f"NowPayments check_payment_status error: {e}")
|
169
|
+
return ProviderResponse(
|
170
|
+
success=False,
|
171
|
+
error_message=str(e)
|
172
|
+
)
|
173
|
+
|
174
|
+
def process_webhook(self, payload: dict) -> WebhookData:
|
175
|
+
"""Process NowPayments webhook."""
|
176
|
+
try:
|
177
|
+
# Map status
|
178
|
+
status_mapping = {
|
179
|
+
'waiting': 'pending',
|
180
|
+
'confirming': 'processing',
|
181
|
+
'confirmed': 'completed',
|
182
|
+
'sending': 'processing',
|
183
|
+
'partially_paid': 'pending',
|
184
|
+
'finished': 'completed',
|
185
|
+
'failed': 'failed',
|
186
|
+
'refunded': 'refunded',
|
187
|
+
'expired': 'expired'
|
188
|
+
}
|
189
|
+
|
190
|
+
provider_status = payload.get('payment_status', 'unknown')
|
191
|
+
universal_status = status_mapping.get(provider_status, 'unknown')
|
192
|
+
|
193
|
+
return WebhookData(
|
194
|
+
provider_payment_id=str(payload.get('payment_id', '')),
|
195
|
+
status=universal_status,
|
196
|
+
pay_amount=Decimal(str(payload.get('pay_amount', 0))),
|
197
|
+
actually_paid=Decimal(str(payload.get('actually_paid', 0))),
|
198
|
+
order_id=payload.get('order_id'),
|
199
|
+
signature=payload.get('signature')
|
200
|
+
)
|
201
|
+
|
202
|
+
except Exception as e:
|
203
|
+
logger.error(f"NowPayments webhook processing error: {e}")
|
204
|
+
raise
|
205
|
+
|
206
|
+
def get_supported_currencies(self) -> List[str]:
|
207
|
+
"""Get list of supported currencies."""
|
208
|
+
try:
|
209
|
+
response = self._make_request('GET', 'currencies')
|
210
|
+
|
211
|
+
if response and 'currencies' in response:
|
212
|
+
return response['currencies']
|
213
|
+
else:
|
214
|
+
# Fallback currencies
|
215
|
+
return ['BTC', 'ETH', 'LTC', 'BCH', 'XMR', 'TRX', 'BNB']
|
216
|
+
|
217
|
+
except Exception as e:
|
218
|
+
logger.error(f"Error getting supported currencies: {e}")
|
219
|
+
return ['BTC', 'ETH', 'LTC'] # Minimal fallback
|
220
|
+
|
221
|
+
def get_minimum_payment_amount(self, currency_from: str, currency_to: str = 'usd') -> Optional[Decimal]:
|
222
|
+
"""Get minimum payment amount for currency pair."""
|
223
|
+
try:
|
224
|
+
response = self._make_request('GET', 'min-amount', {
|
225
|
+
'currency_from': currency_from,
|
226
|
+
'currency_to': currency_to
|
227
|
+
})
|
228
|
+
|
229
|
+
if response and 'min_amount' in response:
|
230
|
+
return Decimal(str(response['min_amount']))
|
231
|
+
|
232
|
+
return None
|
233
|
+
|
234
|
+
except Exception as e:
|
235
|
+
logger.error(f"Error getting minimum amount: {e}")
|
236
|
+
return None
|
237
|
+
|
238
|
+
def estimate_payment_amount(self, amount: Decimal, currency_code: str) -> Optional[dict]:
|
239
|
+
"""Estimate payment amount in target currency."""
|
240
|
+
try:
|
241
|
+
response = self._make_request('GET', 'estimate', {
|
242
|
+
'amount': float(amount),
|
243
|
+
'currency_from': 'usd',
|
244
|
+
'currency_to': currency_code
|
245
|
+
})
|
246
|
+
|
247
|
+
if response and 'estimated_amount' in response:
|
248
|
+
return {
|
249
|
+
'estimated_amount': Decimal(str(response['estimated_amount'])),
|
250
|
+
'currency_from': response.get('currency_from'),
|
251
|
+
'currency_to': response.get('currency_to'),
|
252
|
+
'fee_amount': Decimal(str(response.get('fee_amount', 0)))
|
253
|
+
}
|
254
|
+
|
255
|
+
return None
|
256
|
+
|
257
|
+
except Exception as e:
|
258
|
+
logger.error(f"Error estimating payment amount: {e}")
|
259
|
+
return None
|
260
|
+
|
261
|
+
def validate_webhook(self, payload: dict, headers: Optional[dict] = None) -> bool:
|
262
|
+
"""Validate NowPayments webhook signature."""
|
263
|
+
try:
|
264
|
+
if not self.ipn_secret:
|
265
|
+
logger.warning("IPN secret not configured, skipping webhook validation")
|
266
|
+
return True
|
267
|
+
|
268
|
+
if not headers:
|
269
|
+
logger.warning("No headers provided for webhook validation")
|
270
|
+
return False
|
271
|
+
|
272
|
+
# Get signature from headers
|
273
|
+
signature = headers.get('x-nowpayments-sig')
|
274
|
+
if not signature:
|
275
|
+
logger.warning("No signature found in webhook headers")
|
276
|
+
return False
|
277
|
+
|
278
|
+
# TODO: Implement proper HMAC signature validation
|
279
|
+
# This requires the raw payload body for proper validation
|
280
|
+
logger.info("Webhook signature validation placeholder")
|
281
|
+
return True
|
282
|
+
|
283
|
+
except Exception as e:
|
284
|
+
logger.error(f"Webhook validation error: {e}")
|
285
|
+
return False
|
286
|
+
|
287
|
+
def check_api_status(self) -> bool:
|
288
|
+
"""Check if NowPayments API is available."""
|
289
|
+
try:
|
290
|
+
response = self._make_request('GET', 'status')
|
291
|
+
return response is not None and response.get('message') == 'OK'
|
292
|
+
except:
|
293
|
+
return False
|
@@ -0,0 +1,99 @@
|
|
1
|
+
"""
|
2
|
+
Provider registry for managing payment providers.
|
3
|
+
|
4
|
+
Central registry with lazy loading and typed configuration.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
from typing import Optional, List
|
9
|
+
|
10
|
+
from .base import PaymentProvider
|
11
|
+
from .nowpayments import NowPaymentsProvider, NowPaymentsConfig
|
12
|
+
from .cryptapi import CryptAPIProvider, CryptAPIConfig
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
class ProviderRegistry:
|
18
|
+
"""Central registry for payment providers with typed configs."""
|
19
|
+
|
20
|
+
def __init__(self):
|
21
|
+
"""Initialize registry with lazy loading."""
|
22
|
+
self._providers: dict[str, PaymentProvider] = {}
|
23
|
+
self._provider_configs: dict[str, dict] = {}
|
24
|
+
self._load_configurations()
|
25
|
+
|
26
|
+
def _load_configurations(self) -> None:
|
27
|
+
"""Load provider configurations."""
|
28
|
+
try:
|
29
|
+
from ...utils.config_utils import get_payments_config
|
30
|
+
config = get_payments_config()
|
31
|
+
|
32
|
+
self._provider_configs = {}
|
33
|
+
for provider_name, provider_config in config.providers.items():
|
34
|
+
if provider_config.enabled:
|
35
|
+
self._provider_configs[provider_name] = provider_config.get_config_dict()
|
36
|
+
|
37
|
+
except Exception as e:
|
38
|
+
logger.warning(f"Failed to load provider configurations: {e}")
|
39
|
+
self._provider_configs = {}
|
40
|
+
|
41
|
+
def _create_provider(self, name: str, config_dict: dict) -> Optional[PaymentProvider]:
|
42
|
+
"""Create provider instance from configuration with typed config."""
|
43
|
+
try:
|
44
|
+
if name == 'nowpayments':
|
45
|
+
config = NowPaymentsConfig(**config_dict)
|
46
|
+
return NowPaymentsProvider(config)
|
47
|
+
elif name == 'cryptapi':
|
48
|
+
config = CryptAPIConfig(**config_dict)
|
49
|
+
return CryptAPIProvider(config)
|
50
|
+
elif name == 'stripe':
|
51
|
+
# TODO: Implement StripeProvider with StripeConfig
|
52
|
+
return None
|
53
|
+
else:
|
54
|
+
logger.warning(f"Unknown provider type: {name}")
|
55
|
+
return None
|
56
|
+
|
57
|
+
except Exception as e:
|
58
|
+
logger.error(f"Failed to create provider {name}: {e}")
|
59
|
+
return None
|
60
|
+
|
61
|
+
def register_provider(self, name: str, provider: PaymentProvider) -> None:
|
62
|
+
"""Register a payment provider instance."""
|
63
|
+
self._providers[name] = provider
|
64
|
+
|
65
|
+
def get_provider(self, name: str) -> Optional[PaymentProvider]:
|
66
|
+
"""Get provider by name with lazy loading."""
|
67
|
+
# Check if already loaded
|
68
|
+
if name in self._providers:
|
69
|
+
return self._providers[name]
|
70
|
+
|
71
|
+
# Try to load from configuration
|
72
|
+
if name in self._provider_configs:
|
73
|
+
provider = self._create_provider(name, self._provider_configs[name])
|
74
|
+
if provider:
|
75
|
+
self._providers[name] = provider
|
76
|
+
return provider
|
77
|
+
|
78
|
+
return None
|
79
|
+
|
80
|
+
def list_providers(self) -> List[str]:
|
81
|
+
"""Get list of available providers."""
|
82
|
+
available = set(self._providers.keys())
|
83
|
+
available.update(self._provider_configs.keys())
|
84
|
+
return list(available)
|
85
|
+
|
86
|
+
def get_active_providers(self) -> List[str]:
|
87
|
+
"""Get list of active providers."""
|
88
|
+
active = []
|
89
|
+
for name in self.list_providers():
|
90
|
+
provider = self.get_provider(name)
|
91
|
+
if provider and provider.is_enabled():
|
92
|
+
active.append(name)
|
93
|
+
return active
|
94
|
+
|
95
|
+
def reload_providers(self) -> None:
|
96
|
+
"""Reload all providers from configuration."""
|
97
|
+
logger.info("Reloading providers from configuration")
|
98
|
+
self._providers.clear()
|
99
|
+
self._load_configurations()
|
@@ -0,0 +1,13 @@
|
|
1
|
+
"""
|
2
|
+
Universal Payment Signals.
|
3
|
+
|
4
|
+
Automatically imports all signal handlers when the payments app is loaded.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from .api_key_signals import * # noqa: F401,F403
|
8
|
+
from .payment_signals import * # noqa: F401,F403
|
9
|
+
from .subscription_signals import * # noqa: F401,F403
|
10
|
+
|
11
|
+
__all__ = [
|
12
|
+
# Signal functions are automatically exported by Django
|
13
|
+
]
|
@@ -0,0 +1,150 @@
|
|
1
|
+
"""
|
2
|
+
🔄 Universal API Keys Auto-Creation Signals
|
3
|
+
|
4
|
+
Automatic API key creation and management via Django signals.
|
5
|
+
Enhanced version of CarAPI signals with universal support.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from django.db.models.signals import post_save, post_delete, pre_save
|
9
|
+
from django.dispatch import receiver
|
10
|
+
from django.contrib.auth import get_user_model
|
11
|
+
from django.db import transaction
|
12
|
+
from django.utils import timezone
|
13
|
+
import logging
|
14
|
+
|
15
|
+
from ..models import APIKey
|
16
|
+
|
17
|
+
User = get_user_model()
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
@receiver(post_save, sender=User)
|
22
|
+
def create_default_api_key(sender, instance, created, **kwargs):
|
23
|
+
"""
|
24
|
+
Automatically create default API key for new users.
|
25
|
+
This ensures every user can immediately start using the API.
|
26
|
+
"""
|
27
|
+
if created:
|
28
|
+
try:
|
29
|
+
with transaction.atomic():
|
30
|
+
api_key = APIKey.objects.create(
|
31
|
+
user=instance,
|
32
|
+
name="Default API Key",
|
33
|
+
is_active=True
|
34
|
+
)
|
35
|
+
|
36
|
+
logger.info(
|
37
|
+
f"Created default API key for user {instance.email}: {api_key.key_prefix}***"
|
38
|
+
)
|
39
|
+
|
40
|
+
# Optional: Send welcome email with API key info
|
41
|
+
# This would be handled in custom project implementations
|
42
|
+
# from .tasks import send_api_key_welcome_email
|
43
|
+
# send_api_key_welcome_email.delay(instance.id, api_key.id)
|
44
|
+
|
45
|
+
except Exception as e:
|
46
|
+
logger.error(f"Failed to create default API key for user {instance.email}: {e}")
|
47
|
+
|
48
|
+
|
49
|
+
@receiver(post_save, sender=User)
|
50
|
+
def ensure_user_has_api_key(sender, instance, **kwargs):
|
51
|
+
"""
|
52
|
+
Ensure user always has at least one API key.
|
53
|
+
Creates one if user has no active keys.
|
54
|
+
"""
|
55
|
+
# Skip if this is a new user (handled by create_default_api_key)
|
56
|
+
if kwargs.get('created', False):
|
57
|
+
return
|
58
|
+
|
59
|
+
# Check if user has any active keys
|
60
|
+
if not APIKey.objects.filter(user=instance, is_active=True).exists():
|
61
|
+
try:
|
62
|
+
with transaction.atomic():
|
63
|
+
api_key = APIKey.objects.create(
|
64
|
+
user=instance,
|
65
|
+
name="Recovery API Key",
|
66
|
+
is_active=True
|
67
|
+
)
|
68
|
+
logger.info(
|
69
|
+
f"Created recovery API key for user {instance.email}: {api_key.key_prefix}***"
|
70
|
+
)
|
71
|
+
except Exception as e:
|
72
|
+
logger.error(f"Failed to create recovery API key for user {instance.email}: {e}")
|
73
|
+
|
74
|
+
|
75
|
+
@receiver(pre_save, sender=APIKey)
|
76
|
+
def store_original_status(sender, instance, **kwargs):
|
77
|
+
"""Store original status for change detection."""
|
78
|
+
if instance.pk:
|
79
|
+
try:
|
80
|
+
old_instance = APIKey.objects.get(pk=instance.pk)
|
81
|
+
instance._original_is_active = old_instance.is_active
|
82
|
+
except APIKey.DoesNotExist:
|
83
|
+
instance._original_is_active = None
|
84
|
+
|
85
|
+
|
86
|
+
@receiver(post_save, sender=APIKey)
|
87
|
+
def log_api_key_changes(sender, instance, created, **kwargs):
|
88
|
+
"""Log API key creation and status changes for security monitoring."""
|
89
|
+
if created:
|
90
|
+
logger.info(
|
91
|
+
f"New API key created: {instance.name} ({instance.key_prefix}***) "
|
92
|
+
f"for user {instance.user.email}"
|
93
|
+
)
|
94
|
+
else:
|
95
|
+
# Check if status changed
|
96
|
+
if hasattr(instance, '_original_is_active'):
|
97
|
+
old_status = instance._original_is_active
|
98
|
+
new_status = instance.is_active
|
99
|
+
|
100
|
+
if old_status is not None and old_status != new_status:
|
101
|
+
status_text = "activated" if new_status else "deactivated"
|
102
|
+
logger.warning(
|
103
|
+
f"API key {status_text}: {instance.name} ({instance.key_prefix}***) "
|
104
|
+
f"for user {instance.user.email}"
|
105
|
+
)
|
106
|
+
|
107
|
+
|
108
|
+
@receiver(post_save, sender=APIKey)
|
109
|
+
def update_last_used_on_activation(sender, instance, created, **kwargs):
|
110
|
+
"""Update last_used when API key is activated."""
|
111
|
+
if not created and instance.is_active and hasattr(instance, '_original_is_active'):
|
112
|
+
if instance._original_is_active is False and instance.is_active is True:
|
113
|
+
# Key was just activated
|
114
|
+
APIKey.objects.filter(pk=instance.pk).update(
|
115
|
+
last_used=timezone.now()
|
116
|
+
)
|
117
|
+
|
118
|
+
|
119
|
+
@receiver(post_delete, sender=APIKey)
|
120
|
+
def log_api_key_deletion(sender, instance, **kwargs):
|
121
|
+
"""Log API key deletions for security audit."""
|
122
|
+
logger.warning(
|
123
|
+
f"API key deleted: {instance.name} ({instance.key_prefix}***) "
|
124
|
+
f"for user {instance.user.email} - Status was: {'active' if instance.is_active else 'inactive'}"
|
125
|
+
)
|
126
|
+
|
127
|
+
|
128
|
+
@receiver(post_delete, sender=APIKey)
|
129
|
+
def ensure_user_has_remaining_key(sender, instance, **kwargs):
|
130
|
+
"""
|
131
|
+
Ensure user still has at least one API key after deletion.
|
132
|
+
Creates a new one if this was the last active key.
|
133
|
+
"""
|
134
|
+
user = instance.user
|
135
|
+
|
136
|
+
# Check if user has any remaining active keys
|
137
|
+
if not APIKey.objects.filter(user=user, is_active=True).exists():
|
138
|
+
try:
|
139
|
+
with transaction.atomic():
|
140
|
+
api_key = APIKey.objects.create(
|
141
|
+
user=user,
|
142
|
+
name="Auto Recovery API Key",
|
143
|
+
is_active=True
|
144
|
+
)
|
145
|
+
logger.info(
|
146
|
+
f"Created auto-recovery API key for user {user.email}: {api_key.key_prefix}*** "
|
147
|
+
f"(previous key was deleted)"
|
148
|
+
)
|
149
|
+
except Exception as e:
|
150
|
+
logger.error(f"Failed to create auto-recovery API key for user {user.email}: {e}")
|
@@ -0,0 +1,127 @@
|
|
1
|
+
"""
|
2
|
+
🔄 Universal Payment Signals
|
3
|
+
|
4
|
+
Automatic payment processing and balance management via Django signals.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from django.db.models.signals import post_save, pre_save
|
8
|
+
from django.dispatch import receiver
|
9
|
+
from django.db import transaction
|
10
|
+
from django.utils import timezone
|
11
|
+
import logging
|
12
|
+
|
13
|
+
from ..models import UniversalPayment, UserBalance, Transaction
|
14
|
+
from ..services.cache import SimpleCache
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
@receiver(pre_save, sender=UniversalPayment)
|
20
|
+
def store_original_payment_status(sender, instance, **kwargs):
|
21
|
+
"""Store original payment status for change detection."""
|
22
|
+
if instance.pk:
|
23
|
+
try:
|
24
|
+
old_instance = UniversalPayment.objects.get(pk=instance.pk)
|
25
|
+
instance._original_status = old_instance.status
|
26
|
+
except UniversalPayment.DoesNotExist:
|
27
|
+
instance._original_status = None
|
28
|
+
|
29
|
+
|
30
|
+
@receiver(post_save, sender=UniversalPayment)
|
31
|
+
def process_payment_status_changes(sender, instance, created, **kwargs):
|
32
|
+
"""Process payment status changes and update user balance."""
|
33
|
+
if created:
|
34
|
+
logger.info(f"New payment created: {instance.internal_payment_id} for user {instance.user.email}")
|
35
|
+
return
|
36
|
+
|
37
|
+
# Check if status changed to completed
|
38
|
+
if hasattr(instance, '_original_status'):
|
39
|
+
old_status = instance._original_status
|
40
|
+
new_status = instance.status
|
41
|
+
|
42
|
+
if old_status != new_status:
|
43
|
+
logger.info(
|
44
|
+
f"Payment status changed: {instance.internal_payment_id} "
|
45
|
+
f"for user {instance.user.email} - {old_status} → {new_status}"
|
46
|
+
)
|
47
|
+
|
48
|
+
# Process completed payment
|
49
|
+
if new_status == UniversalPayment.PaymentStatus.COMPLETED and old_status != new_status:
|
50
|
+
_process_completed_payment(instance)
|
51
|
+
|
52
|
+
|
53
|
+
def _process_completed_payment(payment: UniversalPayment):
|
54
|
+
"""Process completed payment and add funds to user balance."""
|
55
|
+
try:
|
56
|
+
with transaction.atomic():
|
57
|
+
# Get or create user balance
|
58
|
+
balance, created = UserBalance.objects.get_or_create(
|
59
|
+
user=payment.user,
|
60
|
+
defaults={
|
61
|
+
'amount_usd': 0,
|
62
|
+
'reserved_usd': 0
|
63
|
+
}
|
64
|
+
)
|
65
|
+
|
66
|
+
# Add funds to balance
|
67
|
+
old_balance = balance.amount_usd
|
68
|
+
balance.amount_usd += payment.amount_usd
|
69
|
+
balance.save()
|
70
|
+
|
71
|
+
# Create transaction record
|
72
|
+
Transaction.objects.create(
|
73
|
+
user=payment.user,
|
74
|
+
transaction_type=Transaction.TransactionType.PAYMENT,
|
75
|
+
amount_usd=payment.amount_usd,
|
76
|
+
balance_before=old_balance,
|
77
|
+
balance_after=balance.amount_usd,
|
78
|
+
description=f"Payment completed: {payment.internal_payment_id}",
|
79
|
+
payment=payment,
|
80
|
+
metadata={
|
81
|
+
'provider': payment.provider,
|
82
|
+
'provider_payment_id': payment.provider_payment_id,
|
83
|
+
'original_amount': str(payment.original_amount),
|
84
|
+
'original_currency': payment.original_currency
|
85
|
+
}
|
86
|
+
)
|
87
|
+
|
88
|
+
# Mark payment as processed
|
89
|
+
payment.processed_at = timezone.now()
|
90
|
+
payment.save(update_fields=['processed_at'])
|
91
|
+
|
92
|
+
# Clear Redis cache for user
|
93
|
+
try:
|
94
|
+
redis_service = RedisService()
|
95
|
+
redis_service.invalidate_user_cache(payment.user.id)
|
96
|
+
except Exception as e:
|
97
|
+
logger.warning(f"Failed to clear Redis cache for user {payment.user.id}: {e}")
|
98
|
+
|
99
|
+
logger.info(
|
100
|
+
f"Payment {payment.internal_payment_id} processed successfully. "
|
101
|
+
f"User {payment.user.email} balance: ${balance.amount_usd}"
|
102
|
+
)
|
103
|
+
|
104
|
+
except Exception as e:
|
105
|
+
logger.error(f"Error processing completed payment {payment.internal_payment_id}: {e}")
|
106
|
+
raise
|
107
|
+
|
108
|
+
|
109
|
+
@receiver(post_save, sender=UniversalPayment)
|
110
|
+
def log_payment_webhook_data(sender, instance, created, **kwargs):
|
111
|
+
"""Log webhook data for audit purposes."""
|
112
|
+
if not created and instance.webhook_data:
|
113
|
+
logger.info(
|
114
|
+
f"Webhook data received for payment {instance.internal_payment_id}: "
|
115
|
+
f"status={instance.status}, provider={instance.provider}"
|
116
|
+
)
|
117
|
+
|
118
|
+
|
119
|
+
@receiver(post_save, sender=Transaction)
|
120
|
+
def log_transaction_creation(sender, instance, created, **kwargs):
|
121
|
+
"""Log transaction creation for audit trail."""
|
122
|
+
if created:
|
123
|
+
logger.info(
|
124
|
+
f"New transaction: {instance.transaction_type} "
|
125
|
+
f"${instance.amount_usd} for user {instance.user.email} "
|
126
|
+
f"(balance: ${instance.balance_after})"
|
127
|
+
)
|