geek-cafe-saas-sdk 0.7.0__py3-none-any.whl → 0.7.2__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 geek-cafe-saas-sdk might be problematic. Click here for more details.
- geek_cafe_saas_sdk/__init__.py +1 -1
- geek_cafe_saas_sdk/domains/files/models/directory.py +42 -6
- geek_cafe_saas_sdk/domains/files/models/file.py +40 -4
- geek_cafe_saas_sdk/domains/files/models/file_share.py +33 -0
- geek_cafe_saas_sdk/domains/files/models/file_version.py +24 -0
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +54 -135
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py +60 -136
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +43 -104
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +57 -131
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +55 -7
- geek_cafe_saas_sdk/domains/notifications/__init__.py +18 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/__init__.py +1 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +73 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +40 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +34 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +43 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +40 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +40 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +83 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +45 -0
- geek_cafe_saas_sdk/domains/notifications/models/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/notifications/models/notification.py +717 -0
- geek_cafe_saas_sdk/domains/notifications/models/notification_preference.py +365 -0
- geek_cafe_saas_sdk/domains/notifications/models/webhook_subscription.py +339 -0
- geek_cafe_saas_sdk/domains/notifications/services/__init__.py +10 -0
- geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +576 -0
- geek_cafe_saas_sdk/domains/payments/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/payments/handlers/README.md +334 -0
- geek_cafe_saas_sdk/domains/payments/handlers/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +105 -0
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +97 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +97 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +68 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +118 -0
- geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +89 -0
- geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/models/__init__.py +17 -0
- geek_cafe_saas_sdk/domains/payments/models/billing_account.py +521 -0
- geek_cafe_saas_sdk/domains/payments/models/payment.py +639 -0
- geek_cafe_saas_sdk/domains/payments/models/payment_intent_ref.py +539 -0
- geek_cafe_saas_sdk/domains/payments/models/refund.py +404 -0
- geek_cafe_saas_sdk/domains/payments/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/payments/services/payment_service.py +405 -0
- geek_cafe_saas_sdk/domains/subscriptions/__init__.py +19 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +408 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/__init__.py +1 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +81 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +48 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +54 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +54 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +83 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +47 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +62 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +82 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +48 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +66 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +54 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +72 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +89 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/addon.py +604 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/discount.py +492 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/plan.py +569 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/usage_record.py +300 -0
- geek_cafe_saas_sdk/domains/subscriptions/services/__init__.py +10 -0
- geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +694 -0
- geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +123 -1
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +213 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +7 -0
- geek_cafe_saas_sdk/services/database_service.py +10 -6
- geek_cafe_saas_sdk/utilities/environment_variables.py +16 -0
- geek_cafe_saas_sdk/utilities/logging_utility.py +77 -0
- {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.2.dist-info}/METADATA +1 -1
- {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.2.dist-info}/RECORD +79 -20
- {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.2.dist-info}/WHEEL +0 -0
- {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Notification Model - Multi-channel notification delivery system.
|
|
3
|
+
|
|
4
|
+
Supports email, SMS, in-app, push notifications with state tracking,
|
|
5
|
+
priority management, and retry logic.
|
|
6
|
+
|
|
7
|
+
Geek Cafe, LLC
|
|
8
|
+
MIT License. See Project Root for the license information.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Optional, Dict, List, Any
|
|
12
|
+
from geek_cafe_saas_sdk.models.base_model import BaseModel
|
|
13
|
+
from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Notification(BaseModel):
|
|
17
|
+
"""
|
|
18
|
+
Notification model for multi-channel message delivery.
|
|
19
|
+
|
|
20
|
+
Handles state management (queued→sending→sent→delivered→failed),
|
|
21
|
+
retry logic, priority, and channel-specific configuration.
|
|
22
|
+
|
|
23
|
+
Channels supported:
|
|
24
|
+
- email: Via SES, SendGrid, etc.
|
|
25
|
+
- sms: Via Twilio, SNS, etc.
|
|
26
|
+
- in_app: Browser/mobile app notifications
|
|
27
|
+
- push: Mobile push notifications (FCM, APNS)
|
|
28
|
+
|
|
29
|
+
States:
|
|
30
|
+
- queued: Created, waiting to send
|
|
31
|
+
- sending: Currently being sent
|
|
32
|
+
- sent: Successfully sent to provider
|
|
33
|
+
- delivered: Confirmed delivery (when available)
|
|
34
|
+
- failed: Delivery failed
|
|
35
|
+
- cancelled: Cancelled before sending
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# Channel types
|
|
39
|
+
CHANNEL_EMAIL = "email"
|
|
40
|
+
CHANNEL_SMS = "sms"
|
|
41
|
+
CHANNEL_IN_APP = "in_app"
|
|
42
|
+
CHANNEL_PUSH = "push"
|
|
43
|
+
CHANNEL_WEBHOOK = "webhook"
|
|
44
|
+
|
|
45
|
+
# States
|
|
46
|
+
STATE_QUEUED = "queued"
|
|
47
|
+
STATE_SENDING = "sending"
|
|
48
|
+
STATE_SENT = "sent"
|
|
49
|
+
STATE_DELIVERED = "delivered"
|
|
50
|
+
STATE_FAILED = "failed"
|
|
51
|
+
STATE_CANCELLED = "cancelled"
|
|
52
|
+
|
|
53
|
+
# Priority levels
|
|
54
|
+
PRIORITY_LOW = "low"
|
|
55
|
+
PRIORITY_NORMAL = "normal"
|
|
56
|
+
PRIORITY_HIGH = "high"
|
|
57
|
+
PRIORITY_URGENT = "urgent"
|
|
58
|
+
|
|
59
|
+
def __init__(self):
|
|
60
|
+
super().__init__()
|
|
61
|
+
|
|
62
|
+
# Core fields
|
|
63
|
+
self._notification_type: str = "" # "welcome_email", "payment_receipt", "alert", etc.
|
|
64
|
+
self._channel: str = self.CHANNEL_EMAIL # Delivery channel
|
|
65
|
+
self._state: str = self.STATE_QUEUED # Current state
|
|
66
|
+
self._priority: str = self.PRIORITY_NORMAL
|
|
67
|
+
|
|
68
|
+
# Recipient
|
|
69
|
+
self._recipient_id: str | None = None # User ID receiving notification
|
|
70
|
+
self._recipient_email: str | None = None
|
|
71
|
+
self._recipient_phone: str | None = None
|
|
72
|
+
self._recipient_device_token: str | None = None # For push
|
|
73
|
+
self._recipient_name: str | None = None
|
|
74
|
+
|
|
75
|
+
# Content
|
|
76
|
+
self._subject: str | None = None # For email
|
|
77
|
+
self._title: str | None = None # For push/in-app
|
|
78
|
+
self._body: str = "" # Main content
|
|
79
|
+
self._body_html: str | None = None # HTML version for email
|
|
80
|
+
self._template_id: str | None = None # Template reference
|
|
81
|
+
self._template_data: Dict[str, Any] = {} # Template variables
|
|
82
|
+
|
|
83
|
+
# Delivery configuration
|
|
84
|
+
self._send_after_utc_ts: float | None = None # Scheduled delivery
|
|
85
|
+
self._expires_utc_ts: float | None = None # Don't send after this time
|
|
86
|
+
self._max_retries: int = 3
|
|
87
|
+
self._retry_count: int = 0
|
|
88
|
+
self._retry_delay_seconds: int = 300 # 5 minutes
|
|
89
|
+
|
|
90
|
+
# Channel-specific config
|
|
91
|
+
self._email_config: Dict[str, Any] = {} # reply-to, cc, bcc, attachments
|
|
92
|
+
self._sms_config: Dict[str, Any] = {} # sender_id, message_type
|
|
93
|
+
self._push_config: Dict[str, Any] = {} # badge, sound, data
|
|
94
|
+
self._webhook_config: Dict[str, Any] = {} # url, method, headers
|
|
95
|
+
|
|
96
|
+
# State tracking
|
|
97
|
+
self._queued_utc_ts: float | None = None
|
|
98
|
+
self._sent_utc_ts: float | None = None
|
|
99
|
+
self._delivered_utc_ts: float | None = None
|
|
100
|
+
self._failed_utc_ts: float | None = None
|
|
101
|
+
self._read_utc_ts: float | None = None # For in-app
|
|
102
|
+
|
|
103
|
+
# Provider tracking
|
|
104
|
+
self._provider: str | None = None # "ses", "twilio", "fcm", etc.
|
|
105
|
+
self._provider_message_id: str | None = None # External ID
|
|
106
|
+
self._provider_response: Dict[str, Any] = {}
|
|
107
|
+
|
|
108
|
+
# Error tracking
|
|
109
|
+
self._error_code: str | None = None
|
|
110
|
+
self._error_message: str | None = None
|
|
111
|
+
self._error_details: Dict[str, Any] = {}
|
|
112
|
+
|
|
113
|
+
# Metadata
|
|
114
|
+
self._triggered_by_event: str | None = None # Event that triggered this
|
|
115
|
+
self._related_resource_type: str | None = None # "payment", "subscription", etc.
|
|
116
|
+
self._related_resource_id: str | None = None
|
|
117
|
+
self._campaign_id: str | None = None # Marketing campaign tracking
|
|
118
|
+
self._tags: List[str] = []
|
|
119
|
+
self._metadata: Dict[str, Any] = {}
|
|
120
|
+
|
|
121
|
+
# CRITICAL: Call _setup_indexes() as LAST line in __init__
|
|
122
|
+
self._setup_indexes()
|
|
123
|
+
|
|
124
|
+
def _setup_indexes(self):
|
|
125
|
+
"""Setup DynamoDB indexes for notification queries."""
|
|
126
|
+
|
|
127
|
+
# Primary index: Notification by ID
|
|
128
|
+
primary: DynamoDBIndex = DynamoDBIndex()
|
|
129
|
+
primary.name = "primary"
|
|
130
|
+
primary.partition_key.attribute_name = "pk"
|
|
131
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(("notification", self.id))
|
|
132
|
+
primary.sort_key.attribute_name = "sk"
|
|
133
|
+
primary.sort_key.value = lambda: "metadata"
|
|
134
|
+
self.indexes.add_primary(primary)
|
|
135
|
+
|
|
136
|
+
# GSI1: Notifications by recipient (for user's notification list)
|
|
137
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
138
|
+
gsi.name = "gsi1"
|
|
139
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
140
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("recipient", self.recipient_id))
|
|
141
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
142
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("queued", self.queued_utc_ts))
|
|
143
|
+
self.indexes.add_secondary(gsi)
|
|
144
|
+
|
|
145
|
+
# GSI2: Notifications by tenant and state (for processing queue)
|
|
146
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
147
|
+
gsi.name = "gsi2"
|
|
148
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
149
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
|
|
150
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
151
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("state", self.state), ("queued", self.queued_utc_ts))
|
|
152
|
+
self.indexes.add_secondary(gsi)
|
|
153
|
+
|
|
154
|
+
# ========================================================================
|
|
155
|
+
# Core Properties
|
|
156
|
+
# ========================================================================
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def notification_type(self) -> str:
|
|
160
|
+
"""Notification type identifier."""
|
|
161
|
+
return self._notification_type
|
|
162
|
+
|
|
163
|
+
@notification_type.setter
|
|
164
|
+
def notification_type(self, value: str):
|
|
165
|
+
self._notification_type = value
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def channel(self) -> str:
|
|
169
|
+
"""Delivery channel (email, sms, in_app, push, webhook)."""
|
|
170
|
+
return self._channel
|
|
171
|
+
|
|
172
|
+
@channel.setter
|
|
173
|
+
def channel(self, value: str):
|
|
174
|
+
self._channel = value
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def state(self) -> str:
|
|
178
|
+
"""Current state."""
|
|
179
|
+
return self._state
|
|
180
|
+
|
|
181
|
+
@state.setter
|
|
182
|
+
def state(self, value: str):
|
|
183
|
+
self._state = value
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def priority(self) -> str:
|
|
187
|
+
"""Priority level."""
|
|
188
|
+
return self._priority
|
|
189
|
+
|
|
190
|
+
@priority.setter
|
|
191
|
+
def priority(self, value: str):
|
|
192
|
+
self._priority = value
|
|
193
|
+
|
|
194
|
+
# ========================================================================
|
|
195
|
+
# Recipient Properties
|
|
196
|
+
# ========================================================================
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def recipient_id(self) -> str | None:
|
|
200
|
+
"""User ID."""
|
|
201
|
+
return self._recipient_id
|
|
202
|
+
|
|
203
|
+
@recipient_id.setter
|
|
204
|
+
def recipient_id(self, value: str | None):
|
|
205
|
+
self._recipient_id = value
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def recipient_email(self) -> str | None:
|
|
209
|
+
"""Email address."""
|
|
210
|
+
return self._recipient_email
|
|
211
|
+
|
|
212
|
+
@recipient_email.setter
|
|
213
|
+
def recipient_email(self, value: str | None):
|
|
214
|
+
self._recipient_email = value
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def recipient_phone(self) -> str | None:
|
|
218
|
+
"""Phone number."""
|
|
219
|
+
return self._recipient_phone
|
|
220
|
+
|
|
221
|
+
@recipient_phone.setter
|
|
222
|
+
def recipient_phone(self, value: str | None):
|
|
223
|
+
self._recipient_phone = value
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def recipient_device_token(self) -> str | None:
|
|
227
|
+
"""Push notification device token."""
|
|
228
|
+
return self._recipient_device_token
|
|
229
|
+
|
|
230
|
+
@recipient_device_token.setter
|
|
231
|
+
def recipient_device_token(self, value: str | None):
|
|
232
|
+
self._recipient_device_token = value
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def recipient_name(self) -> str | None:
|
|
236
|
+
"""Recipient display name."""
|
|
237
|
+
return self._recipient_name
|
|
238
|
+
|
|
239
|
+
@recipient_name.setter
|
|
240
|
+
def recipient_name(self, value: str | None):
|
|
241
|
+
self._recipient_name = value
|
|
242
|
+
|
|
243
|
+
# ========================================================================
|
|
244
|
+
# Content Properties
|
|
245
|
+
# ========================================================================
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def subject(self) -> str | None:
|
|
249
|
+
"""Email subject line."""
|
|
250
|
+
return self._subject
|
|
251
|
+
|
|
252
|
+
@subject.setter
|
|
253
|
+
def subject(self, value: str | None):
|
|
254
|
+
self._subject = value
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def title(self) -> str | None:
|
|
258
|
+
"""Push/in-app notification title."""
|
|
259
|
+
return self._title
|
|
260
|
+
|
|
261
|
+
@title.setter
|
|
262
|
+
def title(self, value: str | None):
|
|
263
|
+
self._title = value
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def body(self) -> str:
|
|
267
|
+
"""Main content body."""
|
|
268
|
+
return self._body
|
|
269
|
+
|
|
270
|
+
@body.setter
|
|
271
|
+
def body(self, value: str):
|
|
272
|
+
self._body = value
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def body_html(self) -> str | None:
|
|
276
|
+
"""HTML body for email."""
|
|
277
|
+
return self._body_html
|
|
278
|
+
|
|
279
|
+
@body_html.setter
|
|
280
|
+
def body_html(self, value: str | None):
|
|
281
|
+
self._body_html = value
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def template_id(self) -> str | None:
|
|
285
|
+
"""Template identifier."""
|
|
286
|
+
return self._template_id
|
|
287
|
+
|
|
288
|
+
@template_id.setter
|
|
289
|
+
def template_id(self, value: str | None):
|
|
290
|
+
self._template_id = value
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def template_data(self) -> Dict[str, Any]:
|
|
294
|
+
"""Template variables."""
|
|
295
|
+
return self._template_data
|
|
296
|
+
|
|
297
|
+
@template_data.setter
|
|
298
|
+
def template_data(self, value: Dict[str, Any]):
|
|
299
|
+
self._template_data = value if value else {}
|
|
300
|
+
|
|
301
|
+
# ========================================================================
|
|
302
|
+
# Delivery Configuration Properties
|
|
303
|
+
# ========================================================================
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def send_after_utc_ts(self) -> float | None:
|
|
307
|
+
"""Scheduled send time."""
|
|
308
|
+
return self._send_after_utc_ts
|
|
309
|
+
|
|
310
|
+
@send_after_utc_ts.setter
|
|
311
|
+
def send_after_utc_ts(self, value: float | None):
|
|
312
|
+
self._send_after_utc_ts = value
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def expires_utc_ts(self) -> float | None:
|
|
316
|
+
"""Expiration time."""
|
|
317
|
+
return self._expires_utc_ts
|
|
318
|
+
|
|
319
|
+
@expires_utc_ts.setter
|
|
320
|
+
def expires_utc_ts(self, value: float | None):
|
|
321
|
+
self._expires_utc_ts = value
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def max_retries(self) -> int:
|
|
325
|
+
"""Maximum retry attempts."""
|
|
326
|
+
return self._max_retries
|
|
327
|
+
|
|
328
|
+
@max_retries.setter
|
|
329
|
+
def max_retries(self, value: int):
|
|
330
|
+
self._max_retries = value
|
|
331
|
+
|
|
332
|
+
@property
|
|
333
|
+
def retry_count(self) -> int:
|
|
334
|
+
"""Current retry count."""
|
|
335
|
+
return self._retry_count
|
|
336
|
+
|
|
337
|
+
@retry_count.setter
|
|
338
|
+
def retry_count(self, value: int):
|
|
339
|
+
self._retry_count = value
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def retry_delay_seconds(self) -> int:
|
|
343
|
+
"""Retry delay in seconds."""
|
|
344
|
+
return self._retry_delay_seconds
|
|
345
|
+
|
|
346
|
+
@retry_delay_seconds.setter
|
|
347
|
+
def retry_delay_seconds(self, value: int):
|
|
348
|
+
self._retry_delay_seconds = value
|
|
349
|
+
|
|
350
|
+
# ========================================================================
|
|
351
|
+
# Channel Config Properties
|
|
352
|
+
# ========================================================================
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def email_config(self) -> Dict[str, Any]:
|
|
356
|
+
"""Email-specific configuration."""
|
|
357
|
+
return self._email_config
|
|
358
|
+
|
|
359
|
+
@email_config.setter
|
|
360
|
+
def email_config(self, value: Dict[str, Any]):
|
|
361
|
+
self._email_config = value if value else {}
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def sms_config(self) -> Dict[str, Any]:
|
|
365
|
+
"""SMS-specific configuration."""
|
|
366
|
+
return self._sms_config
|
|
367
|
+
|
|
368
|
+
@sms_config.setter
|
|
369
|
+
def sms_config(self, value: Dict[str, Any]):
|
|
370
|
+
self._sms_config = value if value else {}
|
|
371
|
+
|
|
372
|
+
@property
|
|
373
|
+
def push_config(self) -> Dict[str, Any]:
|
|
374
|
+
"""Push notification configuration."""
|
|
375
|
+
return self._push_config
|
|
376
|
+
|
|
377
|
+
@push_config.setter
|
|
378
|
+
def push_config(self, value: Dict[str, Any]):
|
|
379
|
+
self._push_config = value if value else {}
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def webhook_config(self) -> Dict[str, Any]:
|
|
383
|
+
"""Webhook configuration."""
|
|
384
|
+
return self._webhook_config
|
|
385
|
+
|
|
386
|
+
@webhook_config.setter
|
|
387
|
+
def webhook_config(self, value: Dict[str, Any]):
|
|
388
|
+
self._webhook_config = value if value else {}
|
|
389
|
+
|
|
390
|
+
# ========================================================================
|
|
391
|
+
# Timestamps
|
|
392
|
+
# ========================================================================
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def queued_utc_ts(self) -> float | None:
|
|
396
|
+
"""When notification was queued."""
|
|
397
|
+
return self._queued_utc_ts
|
|
398
|
+
|
|
399
|
+
@queued_utc_ts.setter
|
|
400
|
+
def queued_utc_ts(self, value: float | None):
|
|
401
|
+
self._queued_utc_ts = value
|
|
402
|
+
|
|
403
|
+
@property
|
|
404
|
+
def sent_utc_ts(self) -> float | None:
|
|
405
|
+
"""When notification was sent."""
|
|
406
|
+
return self._sent_utc_ts
|
|
407
|
+
|
|
408
|
+
@sent_utc_ts.setter
|
|
409
|
+
def sent_utc_ts(self, value: float | None):
|
|
410
|
+
self._sent_utc_ts = value
|
|
411
|
+
|
|
412
|
+
@property
|
|
413
|
+
def delivered_utc_ts(self) -> float | None:
|
|
414
|
+
"""When notification was delivered."""
|
|
415
|
+
return self._delivered_utc_ts
|
|
416
|
+
|
|
417
|
+
@delivered_utc_ts.setter
|
|
418
|
+
def delivered_utc_ts(self, value: float | None):
|
|
419
|
+
self._delivered_utc_ts = value
|
|
420
|
+
|
|
421
|
+
@property
|
|
422
|
+
def failed_utc_ts(self) -> float | None:
|
|
423
|
+
"""When notification failed."""
|
|
424
|
+
return self._failed_utc_ts
|
|
425
|
+
|
|
426
|
+
@failed_utc_ts.setter
|
|
427
|
+
def failed_utc_ts(self, value: float | None):
|
|
428
|
+
self._failed_utc_ts = value
|
|
429
|
+
|
|
430
|
+
@property
|
|
431
|
+
def read_utc_ts(self) -> float | None:
|
|
432
|
+
"""When notification was read (in-app)."""
|
|
433
|
+
return self._read_utc_ts
|
|
434
|
+
|
|
435
|
+
@read_utc_ts.setter
|
|
436
|
+
def read_utc_ts(self, value: float | None):
|
|
437
|
+
self._read_utc_ts = value
|
|
438
|
+
|
|
439
|
+
# ========================================================================
|
|
440
|
+
# Provider Tracking
|
|
441
|
+
# ========================================================================
|
|
442
|
+
|
|
443
|
+
@property
|
|
444
|
+
def provider(self) -> str | None:
|
|
445
|
+
"""Provider name."""
|
|
446
|
+
return self._provider
|
|
447
|
+
|
|
448
|
+
@provider.setter
|
|
449
|
+
def provider(self, value: str | None):
|
|
450
|
+
self._provider = value
|
|
451
|
+
|
|
452
|
+
@property
|
|
453
|
+
def provider_message_id(self) -> str | None:
|
|
454
|
+
"""Provider's message ID."""
|
|
455
|
+
return self._provider_message_id
|
|
456
|
+
|
|
457
|
+
@provider_message_id.setter
|
|
458
|
+
def provider_message_id(self, value: str | None):
|
|
459
|
+
self._provider_message_id = value
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def provider_response(self) -> Dict[str, Any]:
|
|
463
|
+
"""Provider's response data."""
|
|
464
|
+
return self._provider_response
|
|
465
|
+
|
|
466
|
+
@provider_response.setter
|
|
467
|
+
def provider_response(self, value: Dict[str, Any]):
|
|
468
|
+
self._provider_response = value if value else {}
|
|
469
|
+
|
|
470
|
+
# ========================================================================
|
|
471
|
+
# Error Tracking
|
|
472
|
+
# ========================================================================
|
|
473
|
+
|
|
474
|
+
@property
|
|
475
|
+
def error_code(self) -> str | None:
|
|
476
|
+
"""Error code."""
|
|
477
|
+
return self._error_code
|
|
478
|
+
|
|
479
|
+
@error_code.setter
|
|
480
|
+
def error_code(self, value: str | None):
|
|
481
|
+
self._error_code = value
|
|
482
|
+
|
|
483
|
+
@property
|
|
484
|
+
def error_message(self) -> str | None:
|
|
485
|
+
"""Error message."""
|
|
486
|
+
return self._error_message
|
|
487
|
+
|
|
488
|
+
@error_message.setter
|
|
489
|
+
def error_message(self, value: str | None):
|
|
490
|
+
self._error_message = value
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def error_details(self) -> Dict[str, Any]:
|
|
494
|
+
"""Error details."""
|
|
495
|
+
return self._error_details
|
|
496
|
+
|
|
497
|
+
@error_details.setter
|
|
498
|
+
def error_details(self, value: Dict[str, Any]):
|
|
499
|
+
self._error_details = value if value else {}
|
|
500
|
+
|
|
501
|
+
# ========================================================================
|
|
502
|
+
# Metadata
|
|
503
|
+
# ========================================================================
|
|
504
|
+
|
|
505
|
+
@property
|
|
506
|
+
def triggered_by_event(self) -> str | None:
|
|
507
|
+
"""Event that triggered this notification."""
|
|
508
|
+
return self._triggered_by_event
|
|
509
|
+
|
|
510
|
+
@triggered_by_event.setter
|
|
511
|
+
def triggered_by_event(self, value: str | None):
|
|
512
|
+
self._triggered_by_event = value
|
|
513
|
+
|
|
514
|
+
@property
|
|
515
|
+
def related_resource_type(self) -> str | None:
|
|
516
|
+
"""Related resource type."""
|
|
517
|
+
return self._related_resource_type
|
|
518
|
+
|
|
519
|
+
@related_resource_type.setter
|
|
520
|
+
def related_resource_type(self, value: str | None):
|
|
521
|
+
self._related_resource_type = value
|
|
522
|
+
|
|
523
|
+
@property
|
|
524
|
+
def related_resource_id(self) -> str | None:
|
|
525
|
+
"""Related resource ID."""
|
|
526
|
+
return self._related_resource_id
|
|
527
|
+
|
|
528
|
+
@related_resource_id.setter
|
|
529
|
+
def related_resource_id(self, value: str | None):
|
|
530
|
+
self._related_resource_id = value
|
|
531
|
+
|
|
532
|
+
@property
|
|
533
|
+
def campaign_id(self) -> str | None:
|
|
534
|
+
"""Campaign identifier."""
|
|
535
|
+
return self._campaign_id
|
|
536
|
+
|
|
537
|
+
@campaign_id.setter
|
|
538
|
+
def campaign_id(self, value: str | None):
|
|
539
|
+
self._campaign_id = value
|
|
540
|
+
|
|
541
|
+
@property
|
|
542
|
+
def tags(self) -> List[str]:
|
|
543
|
+
"""Tags for organization."""
|
|
544
|
+
return self._tags
|
|
545
|
+
|
|
546
|
+
@tags.setter
|
|
547
|
+
def tags(self, value: List[str]):
|
|
548
|
+
self._tags = value if value else []
|
|
549
|
+
|
|
550
|
+
@property
|
|
551
|
+
def metadata(self) -> Dict[str, Any]:
|
|
552
|
+
"""Additional metadata."""
|
|
553
|
+
return self._metadata
|
|
554
|
+
|
|
555
|
+
@metadata.setter
|
|
556
|
+
def metadata(self, value: Dict[str, Any]):
|
|
557
|
+
self._metadata = value if value else {}
|
|
558
|
+
|
|
559
|
+
# ========================================================================
|
|
560
|
+
# Helper Methods
|
|
561
|
+
# ========================================================================
|
|
562
|
+
|
|
563
|
+
def is_queued(self) -> bool:
|
|
564
|
+
"""Check if notification is queued."""
|
|
565
|
+
return self._state == self.STATE_QUEUED
|
|
566
|
+
|
|
567
|
+
def is_sent(self) -> bool:
|
|
568
|
+
"""Check if notification was sent."""
|
|
569
|
+
return self._state in [self.STATE_SENT, self.STATE_DELIVERED]
|
|
570
|
+
|
|
571
|
+
def is_delivered(self) -> bool:
|
|
572
|
+
"""Check if notification was delivered."""
|
|
573
|
+
return self._state == self.STATE_DELIVERED
|
|
574
|
+
|
|
575
|
+
def is_failed(self) -> bool:
|
|
576
|
+
"""Check if notification failed."""
|
|
577
|
+
return self._state == self.STATE_FAILED
|
|
578
|
+
|
|
579
|
+
def is_cancelled(self) -> bool:
|
|
580
|
+
"""Check if notification was cancelled."""
|
|
581
|
+
return self._state == self.STATE_CANCELLED
|
|
582
|
+
|
|
583
|
+
def can_retry(self) -> bool:
|
|
584
|
+
"""Check if notification can be retried."""
|
|
585
|
+
return (
|
|
586
|
+
self._state == self.STATE_FAILED and
|
|
587
|
+
self._retry_count < self._max_retries
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
def is_expired(self, current_ts: float) -> bool:
|
|
591
|
+
"""Check if notification has expired."""
|
|
592
|
+
if not self._expires_utc_ts:
|
|
593
|
+
return False
|
|
594
|
+
return current_ts > self._expires_utc_ts
|
|
595
|
+
|
|
596
|
+
def should_send_now(self, current_ts: float) -> bool:
|
|
597
|
+
"""Check if notification should be sent now."""
|
|
598
|
+
if self._send_after_utc_ts:
|
|
599
|
+
return current_ts >= self._send_after_utc_ts
|
|
600
|
+
return True
|
|
601
|
+
|
|
602
|
+
def mark_sent(self, timestamp: float, provider_id: str | None = None):
|
|
603
|
+
"""Mark notification as sent."""
|
|
604
|
+
self._state = self.STATE_SENT
|
|
605
|
+
self._sent_utc_ts = timestamp
|
|
606
|
+
if provider_id:
|
|
607
|
+
self._provider_message_id = provider_id
|
|
608
|
+
|
|
609
|
+
def mark_delivered(self, timestamp: float):
|
|
610
|
+
"""Mark notification as delivered."""
|
|
611
|
+
self._state = self.STATE_DELIVERED
|
|
612
|
+
self._delivered_utc_ts = timestamp
|
|
613
|
+
|
|
614
|
+
def mark_failed(self, timestamp: float, error_code: str, error_message: str):
|
|
615
|
+
"""Mark notification as failed."""
|
|
616
|
+
self._state = self.STATE_FAILED
|
|
617
|
+
self._failed_utc_ts = timestamp
|
|
618
|
+
self._error_code = error_code
|
|
619
|
+
self._error_message = error_message
|
|
620
|
+
self._retry_count += 1
|
|
621
|
+
|
|
622
|
+
def mark_read(self, timestamp: float):
|
|
623
|
+
"""Mark notification as read (for in-app)."""
|
|
624
|
+
self._read_utc_ts = timestamp
|
|
625
|
+
|
|
626
|
+
def cancel(self):
|
|
627
|
+
"""Cancel the notification."""
|
|
628
|
+
if self._state == self.STATE_QUEUED:
|
|
629
|
+
self._state = self.STATE_CANCELLED
|
|
630
|
+
|
|
631
|
+
def get_recipient_address(self) -> str | None:
|
|
632
|
+
"""Get the appropriate recipient address based on channel."""
|
|
633
|
+
if self._channel == self.CHANNEL_EMAIL:
|
|
634
|
+
return self._recipient_email
|
|
635
|
+
elif self._channel == self.CHANNEL_SMS:
|
|
636
|
+
return self._recipient_phone
|
|
637
|
+
elif self._channel == self.CHANNEL_PUSH:
|
|
638
|
+
return self._recipient_device_token
|
|
639
|
+
elif self._channel == self.CHANNEL_IN_APP:
|
|
640
|
+
return self._recipient_id
|
|
641
|
+
return None
|
|
642
|
+
|
|
643
|
+
# ========================================================================
|
|
644
|
+
# Validation
|
|
645
|
+
# ========================================================================
|
|
646
|
+
|
|
647
|
+
def validate(self) -> tuple[bool, list[str]]:
|
|
648
|
+
"""
|
|
649
|
+
Validate notification data.
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
Tuple of (is_valid, list of error messages)
|
|
653
|
+
"""
|
|
654
|
+
errors = []
|
|
655
|
+
|
|
656
|
+
# Required fields
|
|
657
|
+
if not self._notification_type:
|
|
658
|
+
errors.append("notification_type is required")
|
|
659
|
+
|
|
660
|
+
if not self._channel:
|
|
661
|
+
errors.append("channel is required")
|
|
662
|
+
|
|
663
|
+
if self._channel not in [
|
|
664
|
+
self.CHANNEL_EMAIL,
|
|
665
|
+
self.CHANNEL_SMS,
|
|
666
|
+
self.CHANNEL_IN_APP,
|
|
667
|
+
self.CHANNEL_PUSH,
|
|
668
|
+
self.CHANNEL_WEBHOOK
|
|
669
|
+
]:
|
|
670
|
+
errors.append(f"Invalid channel: {self._channel}")
|
|
671
|
+
|
|
672
|
+
# Channel-specific validation
|
|
673
|
+
if self._channel == self.CHANNEL_EMAIL:
|
|
674
|
+
if not self._recipient_email:
|
|
675
|
+
errors.append("recipient_email is required for email channel")
|
|
676
|
+
if not self._subject and not self._template_id:
|
|
677
|
+
errors.append("subject or template_id is required for email")
|
|
678
|
+
|
|
679
|
+
elif self._channel == self.CHANNEL_SMS:
|
|
680
|
+
if not self._recipient_phone:
|
|
681
|
+
errors.append("recipient_phone is required for SMS channel")
|
|
682
|
+
|
|
683
|
+
elif self._channel == self.CHANNEL_PUSH:
|
|
684
|
+
if not self._recipient_device_token:
|
|
685
|
+
errors.append("recipient_device_token is required for push channel")
|
|
686
|
+
if not self._title and not self._template_id:
|
|
687
|
+
errors.append("title or template_id is required for push")
|
|
688
|
+
|
|
689
|
+
elif self._channel == self.CHANNEL_IN_APP:
|
|
690
|
+
if not self._recipient_id:
|
|
691
|
+
errors.append("recipient_id is required for in-app channel")
|
|
692
|
+
|
|
693
|
+
elif self._channel == self.CHANNEL_WEBHOOK:
|
|
694
|
+
if not self._webhook_config.get("url"):
|
|
695
|
+
errors.append("webhook_config.url is required for webhook channel")
|
|
696
|
+
|
|
697
|
+
# Content validation
|
|
698
|
+
if not self._body and not self._template_id:
|
|
699
|
+
errors.append("body or template_id is required")
|
|
700
|
+
|
|
701
|
+
# Priority validation
|
|
702
|
+
if self._priority not in [
|
|
703
|
+
self.PRIORITY_LOW,
|
|
704
|
+
self.PRIORITY_NORMAL,
|
|
705
|
+
self.PRIORITY_HIGH,
|
|
706
|
+
self.PRIORITY_URGENT
|
|
707
|
+
]:
|
|
708
|
+
errors.append(f"Invalid priority: {self._priority}")
|
|
709
|
+
|
|
710
|
+
# Retry validation
|
|
711
|
+
if self._max_retries < 0:
|
|
712
|
+
errors.append("max_retries must be >= 0")
|
|
713
|
+
|
|
714
|
+
if self._retry_delay_seconds < 0:
|
|
715
|
+
errors.append("retry_delay_seconds must be >= 0")
|
|
716
|
+
|
|
717
|
+
return (len(errors) == 0, errors)
|