geek-cafe-saas-sdk 0.6.0__py3-none-any.whl → 0.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of geek-cafe-saas-sdk might be problematic. Click here for more details.

Files changed (94) hide show
  1. geek_cafe_saas_sdk/__init__.py +2 -2
  2. geek_cafe_saas_sdk/domains/files/handlers/README.md +446 -0
  3. geek_cafe_saas_sdk/domains/files/handlers/__init__.py +6 -0
  4. geek_cafe_saas_sdk/domains/files/handlers/files/create/app.py +121 -0
  5. geek_cafe_saas_sdk/domains/files/handlers/files/download/app.py +80 -0
  6. geek_cafe_saas_sdk/domains/files/handlers/files/get/app.py +62 -0
  7. geek_cafe_saas_sdk/domains/files/handlers/files/list/app.py +72 -0
  8. geek_cafe_saas_sdk/domains/files/handlers/lineage/create_derived/app.py +99 -0
  9. geek_cafe_saas_sdk/domains/files/handlers/lineage/create_main/app.py +104 -0
  10. geek_cafe_saas_sdk/domains/files/handlers/lineage/download_bundle/app.py +99 -0
  11. geek_cafe_saas_sdk/domains/files/handlers/lineage/get_lineage/app.py +68 -0
  12. geek_cafe_saas_sdk/domains/files/handlers/lineage/prepare_bundle/app.py +76 -0
  13. geek_cafe_saas_sdk/domains/files/models/__init__.py +17 -0
  14. geek_cafe_saas_sdk/domains/files/models/directory.py +42 -6
  15. geek_cafe_saas_sdk/domains/files/models/file.py +158 -16
  16. geek_cafe_saas_sdk/domains/files/models/file_share.py +33 -0
  17. geek_cafe_saas_sdk/domains/files/models/file_version.py +24 -0
  18. geek_cafe_saas_sdk/domains/files/services/__init__.py +21 -0
  19. geek_cafe_saas_sdk/domains/files/services/directory_service.py +54 -135
  20. geek_cafe_saas_sdk/domains/files/services/file_lineage_service.py +487 -0
  21. geek_cafe_saas_sdk/domains/files/services/file_share_service.py +37 -120
  22. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +67 -103
  23. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +44 -124
  24. geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +55 -7
  25. geek_cafe_saas_sdk/domains/notifications/__init__.py +18 -0
  26. geek_cafe_saas_sdk/domains/notifications/handlers/__init__.py +1 -0
  27. geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +73 -0
  28. geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +40 -0
  29. geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +34 -0
  30. geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +43 -0
  31. geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +40 -0
  32. geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +40 -0
  33. geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +83 -0
  34. geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +45 -0
  35. geek_cafe_saas_sdk/domains/notifications/models/__init__.py +16 -0
  36. geek_cafe_saas_sdk/domains/notifications/models/notification.py +717 -0
  37. geek_cafe_saas_sdk/domains/notifications/models/notification_preference.py +365 -0
  38. geek_cafe_saas_sdk/domains/notifications/models/webhook_subscription.py +339 -0
  39. geek_cafe_saas_sdk/domains/notifications/services/__init__.py +10 -0
  40. geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +576 -0
  41. geek_cafe_saas_sdk/domains/payments/__init__.py +16 -0
  42. geek_cafe_saas_sdk/domains/payments/handlers/README.md +334 -0
  43. geek_cafe_saas_sdk/domains/payments/handlers/__init__.py +6 -0
  44. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +105 -0
  45. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +60 -0
  46. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +97 -0
  47. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +97 -0
  48. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +60 -0
  49. geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +60 -0
  50. geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +68 -0
  51. geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +118 -0
  52. geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +89 -0
  53. geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +60 -0
  54. geek_cafe_saas_sdk/domains/payments/models/__init__.py +17 -0
  55. geek_cafe_saas_sdk/domains/payments/models/billing_account.py +521 -0
  56. geek_cafe_saas_sdk/domains/payments/models/payment.py +639 -0
  57. geek_cafe_saas_sdk/domains/payments/models/payment_intent_ref.py +539 -0
  58. geek_cafe_saas_sdk/domains/payments/models/refund.py +404 -0
  59. geek_cafe_saas_sdk/domains/payments/services/__init__.py +11 -0
  60. geek_cafe_saas_sdk/domains/payments/services/payment_service.py +405 -0
  61. geek_cafe_saas_sdk/domains/subscriptions/__init__.py +19 -0
  62. geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +408 -0
  63. geek_cafe_saas_sdk/domains/subscriptions/handlers/__init__.py +1 -0
  64. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +81 -0
  65. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +48 -0
  66. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +54 -0
  67. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +54 -0
  68. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +83 -0
  69. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +47 -0
  70. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +62 -0
  71. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +82 -0
  72. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +48 -0
  73. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +66 -0
  74. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +54 -0
  75. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +72 -0
  76. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +89 -0
  77. geek_cafe_saas_sdk/domains/subscriptions/models/__init__.py +13 -0
  78. geek_cafe_saas_sdk/domains/subscriptions/models/addon.py +604 -0
  79. geek_cafe_saas_sdk/domains/subscriptions/models/discount.py +492 -0
  80. geek_cafe_saas_sdk/domains/subscriptions/models/plan.py +569 -0
  81. geek_cafe_saas_sdk/domains/subscriptions/models/usage_record.py +300 -0
  82. geek_cafe_saas_sdk/domains/subscriptions/services/__init__.py +10 -0
  83. geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +694 -0
  84. geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +123 -1
  85. geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +213 -0
  86. geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +7 -0
  87. geek_cafe_saas_sdk/services/database_service.py +10 -6
  88. geek_cafe_saas_sdk/utilities/cognito_utility.py +16 -26
  89. geek_cafe_saas_sdk/utilities/environment_variables.py +16 -0
  90. geek_cafe_saas_sdk/utilities/logging_utility.py +77 -0
  91. {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/METADATA +11 -11
  92. {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/RECORD +94 -23
  93. {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/WHEEL +0 -0
  94. {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,487 @@
1
+ """
2
+ File Lineage Service
3
+
4
+ Helper service for managing file lineage and transformations.
5
+ Works on top of FileSystemService.
6
+
7
+ Geek Cafe, LLC
8
+ MIT License. See Project Root for the license information.
9
+ """
10
+
11
+ from typing import Dict, Any, List, Optional
12
+ from geek_cafe_saas_sdk.domains.files.services.file_system_service import FileSystemService
13
+ from geek_cafe_saas_sdk.domains.files.services.s3_file_service import S3FileService
14
+ from geek_cafe_saas_sdk.domains.files.models.file import File
15
+ from geek_cafe_saas_sdk.core.service_result import ServiceResult
16
+ from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError
17
+ import datetime as dt
18
+
19
+
20
+ class FileLineageService:
21
+ """Service for managing file lineage and transformations."""
22
+
23
+ def __init__(self, file_service: FileSystemService, s3_service: S3FileService):
24
+ """
25
+ Initialize lineage service.
26
+
27
+ Args:
28
+ file_service: FileSystemService instance
29
+ s3_service: S3FileService instance
30
+ """
31
+ self.file_service = file_service
32
+ self.s3_service = s3_service
33
+
34
+ def create_main_file(
35
+ self,
36
+ tenant_id: str,
37
+ user_id: str,
38
+ original_file_id: str,
39
+ file_name: str,
40
+ file_data: bytes,
41
+ mime_type: str,
42
+ transformation_operation: str,
43
+ transformation_metadata: Optional[Dict[str, Any]] = None,
44
+ directory_id: Optional[str] = None
45
+ ) -> ServiceResult[File]:
46
+ """
47
+ Create a main file from an original file.
48
+
49
+ Args:
50
+ tenant_id: Tenant ID
51
+ user_id: User ID
52
+ original_file_id: Original file ID
53
+ file_name: New file name (e.g., "data.csv")
54
+ file_data: Converted file data
55
+ mime_type: MIME type
56
+ transformation_operation: Operation name (e.g., "xls_to_csv")
57
+ transformation_metadata: Additional metadata
58
+ directory_id: Optional directory ID (inherits from original if not provided)
59
+
60
+ Returns:
61
+ ServiceResult with main File
62
+ """
63
+ try:
64
+ # Get original file
65
+ original_result = self.file_service.get_by_id(
66
+ resource_id=original_file_id,
67
+ tenant_id=tenant_id,
68
+ user_id=user_id
69
+ )
70
+
71
+ if not original_result.success:
72
+ return original_result
73
+
74
+ original_file = original_result.data
75
+
76
+ # Validate original is actually an original
77
+ if original_file.file_role != "original":
78
+ return ServiceResult.error_result(
79
+ message="Source file must have role 'original'",
80
+ error_code="INVALID_FILE_ROLE"
81
+ )
82
+
83
+ # Use original's directory if not specified
84
+ target_directory_id = directory_id if directory_id is not None else original_file.directory_id
85
+
86
+ # Create main file
87
+ result = self.file_service.create(
88
+ tenant_id=tenant_id,
89
+ user_id=user_id,
90
+ file_name=file_name,
91
+ file_data=file_data,
92
+ mime_type=mime_type,
93
+ directory_id=target_directory_id,
94
+ file_role="main",
95
+ parent_file_id=original_file_id,
96
+ original_file_id=original_file_id,
97
+ transformation_type="convert",
98
+ transformation_operation=transformation_operation,
99
+ transformation_metadata=transformation_metadata or {}
100
+ )
101
+
102
+ if result.success:
103
+ # Update original file's derived count
104
+ self.file_service.update(
105
+ resource_id=original_file_id,
106
+ tenant_id=tenant_id,
107
+ user_id=user_id,
108
+ updates={'derived_file_count': 1}
109
+ )
110
+
111
+ return result
112
+
113
+ except Exception as e:
114
+ return ServiceResult.error_result(
115
+ message=f"Failed to create main file: {str(e)}",
116
+ error_code="CREATE_MAIN_FILE_FAILED",
117
+ error=e
118
+ )
119
+
120
+ def create_derived_file(
121
+ self,
122
+ tenant_id: str,
123
+ user_id: str,
124
+ main_file_id: str,
125
+ file_name: str,
126
+ file_data: bytes,
127
+ transformation_operation: str,
128
+ transformation_metadata: Optional[Dict[str, Any]] = None,
129
+ directory_id: Optional[str] = None
130
+ ) -> ServiceResult[File]:
131
+ """
132
+ Create a derived file from a main file.
133
+
134
+ Args:
135
+ tenant_id: Tenant ID
136
+ user_id: User ID
137
+ main_file_id: Main file ID (parent)
138
+ file_name: New file name (e.g., "data_clean_v1.csv")
139
+ file_data: Processed file data
140
+ transformation_operation: Operation name (e.g., "data_cleaning_v1")
141
+ transformation_metadata: Additional metadata
142
+ directory_id: Optional directory ID (inherits from main if not provided)
143
+
144
+ Returns:
145
+ ServiceResult with derived File
146
+ """
147
+ try:
148
+ # Get main file
149
+ main_result = self.file_service.get_by_id(
150
+ resource_id=main_file_id,
151
+ tenant_id=tenant_id,
152
+ user_id=user_id
153
+ )
154
+
155
+ if not main_result.success:
156
+ return main_result
157
+
158
+ main_file = main_result.data
159
+
160
+ # Validate main is actually a main file
161
+ if main_file.file_role != "main":
162
+ return ServiceResult.error_result(
163
+ message="Source file must have role 'main'",
164
+ error_code="INVALID_FILE_ROLE"
165
+ )
166
+
167
+ # Use main's directory if not specified
168
+ target_directory_id = directory_id if directory_id is not None else main_file.directory_id
169
+
170
+ # Create derived file
171
+ result = self.file_service.create(
172
+ tenant_id=tenant_id,
173
+ user_id=user_id,
174
+ file_name=file_name,
175
+ file_data=file_data,
176
+ mime_type=main_file.mime_type,
177
+ directory_id=target_directory_id,
178
+ file_role="derived",
179
+ parent_file_id=main_file_id,
180
+ original_file_id=main_file.original_file_id,
181
+ transformation_type="clean",
182
+ transformation_operation=transformation_operation,
183
+ transformation_metadata=transformation_metadata or {}
184
+ )
185
+
186
+ if result.success:
187
+ # Atomically increment main file's derived count
188
+ # Re-fetch to get current count and avoid race conditions
189
+ fresh_main = self.file_service.get_by_id(
190
+ resource_id=main_file_id,
191
+ tenant_id=tenant_id,
192
+ user_id=user_id
193
+ )
194
+
195
+ if fresh_main.success:
196
+ new_count = fresh_main.data.derived_file_count + 1
197
+ self.file_service.update(
198
+ resource_id=main_file_id,
199
+ tenant_id=tenant_id,
200
+ user_id=user_id,
201
+ updates={'derived_file_count': new_count}
202
+ )
203
+
204
+ return result
205
+
206
+ except Exception as e:
207
+ return ServiceResult.error_result(
208
+ message=f"Failed to create derived file: {str(e)}",
209
+ error_code="CREATE_DERIVED_FILE_FAILED",
210
+ error=e
211
+ )
212
+
213
+ def get_lineage(
214
+ self,
215
+ file_id: str,
216
+ tenant_id: str,
217
+ user_id: str
218
+ ) -> ServiceResult[Dict[str, Any]]:
219
+ """
220
+ Get complete lineage for a file.
221
+
222
+ Returns:
223
+ ServiceResult with lineage dict containing:
224
+ - selected: The selected file
225
+ - main: Main file (if exists)
226
+ - original: Original file (if exists)
227
+ - all_derived: List of all derived files (if viewing main)
228
+ """
229
+ try:
230
+ # Get selected file
231
+ selected_result = self.file_service.get_by_id(
232
+ resource_id=file_id,
233
+ tenant_id=tenant_id,
234
+ user_id=user_id
235
+ )
236
+
237
+ if not selected_result.success:
238
+ return selected_result
239
+
240
+ selected_file = selected_result.data
241
+
242
+ lineage = {
243
+ 'selected': selected_file,
244
+ 'main': None,
245
+ 'original': None,
246
+ 'all_derived': []
247
+ }
248
+
249
+ # Get original file
250
+ if selected_file.original_file_id:
251
+ original_result = self.file_service.get_by_id(
252
+ resource_id=selected_file.original_file_id,
253
+ tenant_id=tenant_id,
254
+ user_id=user_id
255
+ )
256
+ if original_result.success:
257
+ lineage['original'] = original_result.data
258
+
259
+ # Get main file
260
+ if selected_file.is_derived() and selected_file.parent_file_id:
261
+ main_result = self.file_service.get_by_id(
262
+ resource_id=selected_file.parent_file_id,
263
+ tenant_id=tenant_id,
264
+ user_id=user_id
265
+ )
266
+ if main_result.success:
267
+ lineage['main'] = main_result.data
268
+ elif selected_file.is_main():
269
+ lineage['main'] = selected_file
270
+
271
+ # Get all derived files if viewing main
272
+ if selected_file.is_main():
273
+ derived = self.list_derived_files(
274
+ main_file_id=file_id,
275
+ tenant_id=tenant_id,
276
+ user_id=user_id
277
+ )
278
+ if derived.success:
279
+ lineage['all_derived'] = derived.data
280
+
281
+ return ServiceResult.success_result(lineage)
282
+
283
+ except Exception as e:
284
+ return ServiceResult.error_result(
285
+ message=f"Failed to get lineage: {str(e)}",
286
+ error_code="GET_LINEAGE_FAILED",
287
+ error=e
288
+ )
289
+
290
+ def list_derived_files(
291
+ self,
292
+ main_file_id: str,
293
+ tenant_id: str,
294
+ user_id: str,
295
+ limit: int = 100
296
+ ) -> ServiceResult[List[File]]:
297
+ """
298
+ List all files derived from a main file.
299
+
300
+ Returns:
301
+ ServiceResult with list of derived Files
302
+ """
303
+ try:
304
+ # Get all user's files
305
+ all_files_result = self.file_service.list_files_by_owner(
306
+ tenant_id=tenant_id,
307
+ owner_id=user_id,
308
+ user_id=user_id,
309
+ limit=limit
310
+ )
311
+
312
+ if not all_files_result.success:
313
+ return all_files_result
314
+
315
+ # Filter to derived files from this main file
316
+ derived_files = [
317
+ f for f in all_files_result.data
318
+ if f.parent_file_id == main_file_id and f.is_derived()
319
+ ]
320
+
321
+ # Sort by creation date
322
+ derived_files.sort(key=lambda f: f.created_utc_ts)
323
+
324
+ return ServiceResult.success_result(derived_files)
325
+
326
+ except Exception as e:
327
+ return ServiceResult.error_result(
328
+ message=f"Failed to list derived files: {str(e)}",
329
+ error_code="LIST_DERIVED_FAILED",
330
+ error=e
331
+ )
332
+
333
+ def prepare_lineage_bundle(
334
+ self,
335
+ selected_file_id: str,
336
+ tenant_id: str,
337
+ user_id: str
338
+ ) -> ServiceResult[Dict[str, Any]]:
339
+ """
340
+ Prepare bundle of files for lineage.
341
+
342
+ Returns:
343
+ ServiceResult with bundle dict containing:
344
+ - selected_file: Selected file
345
+ - main_file: Main file
346
+ - original_file: Original file
347
+ - metadata: Transformation chain info
348
+ """
349
+ try:
350
+ lineage_result = self.get_lineage(
351
+ file_id=selected_file_id,
352
+ tenant_id=tenant_id,
353
+ user_id=user_id
354
+ )
355
+
356
+ if not lineage_result.success:
357
+ return lineage_result
358
+
359
+ lineage = lineage_result.data
360
+
361
+ bundle = {
362
+ 'selected_file': lineage['selected'],
363
+ 'main_file': lineage['main'],
364
+ 'original_file': lineage['original'],
365
+ 'metadata': {
366
+ 'selected_file_id': selected_file_id,
367
+ 'selected_file_name': lineage['selected'].file_name,
368
+ 'transformation_chain': []
369
+ }
370
+ }
371
+
372
+ # Build transformation chain
373
+ if lineage['original']:
374
+ bundle['metadata']['transformation_chain'].append({
375
+ 'step': 1,
376
+ 'type': 'original',
377
+ 'file_id': lineage['original'].file_id,
378
+ 'file_name': lineage['original'].file_name
379
+ })
380
+
381
+ if lineage['main']:
382
+ bundle['metadata']['transformation_chain'].append({
383
+ 'step': 2,
384
+ 'type': 'convert',
385
+ 'file_id': lineage['main'].file_id,
386
+ 'file_name': lineage['main'].file_name,
387
+ 'operation': lineage['main'].transformation_operation
388
+ })
389
+
390
+ if lineage['selected'].is_derived():
391
+ bundle['metadata']['transformation_chain'].append({
392
+ 'step': 3,
393
+ 'type': 'clean',
394
+ 'file_id': lineage['selected'].file_id,
395
+ 'file_name': lineage['selected'].file_name,
396
+ 'operation': lineage['selected'].transformation_operation
397
+ })
398
+
399
+ return ServiceResult.success_result(bundle)
400
+
401
+ except Exception as e:
402
+ return ServiceResult.error_result(
403
+ message=f"Failed to prepare bundle: {str(e)}",
404
+ error_code="PREPARE_BUNDLE_FAILED",
405
+ error=e
406
+ )
407
+
408
+ def download_lineage_bundle(
409
+ self,
410
+ selected_file_id: str,
411
+ tenant_id: str,
412
+ user_id: str
413
+ ) -> ServiceResult[Dict[str, Any]]:
414
+ """
415
+ Download all files in lineage chain.
416
+
417
+ Returns:
418
+ ServiceResult with dict containing:
419
+ - selected: {'file': File, 'data': bytes}
420
+ - main: {'file': File, 'data': bytes}
421
+ - original: {'file': File, 'data': bytes}
422
+ - metadata: Transformation chain info
423
+ """
424
+ try:
425
+ bundle_result = self.prepare_lineage_bundle(
426
+ selected_file_id=selected_file_id,
427
+ tenant_id=tenant_id,
428
+ user_id=user_id
429
+ )
430
+
431
+ if not bundle_result.success:
432
+ return bundle_result
433
+
434
+ bundle = bundle_result.data
435
+ download_bundle = {
436
+ 'selected': None,
437
+ 'main': None,
438
+ 'original': None,
439
+ 'metadata': bundle['metadata']
440
+ }
441
+
442
+ # Download selected file
443
+ selected_download = self.file_service.download_file(
444
+ tenant_id=tenant_id,
445
+ file_id=selected_file_id,
446
+ user_id=user_id
447
+ )
448
+ if selected_download.success:
449
+ download_bundle['selected'] = {
450
+ 'file': selected_download.data['file'],
451
+ 'data': selected_download.data['data']
452
+ }
453
+
454
+ # Download main file
455
+ if bundle['main_file']:
456
+ main_download = self.file_service.download_file(
457
+ tenant_id=tenant_id,
458
+ file_id=bundle['main_file'].file_id,
459
+ user_id=user_id
460
+ )
461
+ if main_download.success:
462
+ download_bundle['main'] = {
463
+ 'file': main_download.data['file'],
464
+ 'data': main_download.data['data']
465
+ }
466
+
467
+ # Download original file
468
+ if bundle['original_file']:
469
+ original_download = self.file_service.download_file(
470
+ tenant_id=tenant_id,
471
+ file_id=bundle['original_file'].file_id,
472
+ user_id=user_id
473
+ )
474
+ if original_download.success:
475
+ download_bundle['original'] = {
476
+ 'file': original_download.data['file'],
477
+ 'data': original_download.data['data']
478
+ }
479
+
480
+ return ServiceResult.success_result(download_bundle)
481
+
482
+ except Exception as e:
483
+ return ServiceResult.error_result(
484
+ message=f"Failed to download bundle: {str(e)}",
485
+ error_code="DOWNLOAD_BUNDLE_FAILED",
486
+ error=e
487
+ )