jebin2 commited on
Commit
8e4a45e
Β·
1 Parent(s): 7a09a34
ARCHITECTURE_AUDIT_REPORT.md DELETED
@@ -1,331 +0,0 @@
1
- # Codebase Architecture Audit Report
2
-
3
- ## Executive Summary
4
-
5
- Completed comprehensive audit of the codebase to identify services and patterns that could benefit from refactoring similar to the credit service middleware-centric approach.
6
-
7
- **Audit Date:** 2025-12-15
8
- **Files Analyzed:** 50+ files across routers/, services/, core/
9
- **Focus Areas:** Service architecture, transaction patterns, middleware opportunities
10
-
11
- ---
12
-
13
- ## βœ… What's Already Good
14
-
15
- ### 1. Clean Router Layer
16
- - βœ… **No direct database commits** in routers
17
- - βœ… All routers use `QueryService` for data access
18
- - βœ… Proper dependency injection pattern
19
- - βœ… Auth handled by middleware (not manual in each endpoint)
20
-
21
- ### 2. Well-Structured Services
22
- - βœ… `CreditService` - Fully middleware-driven with transaction tracking
23
- - βœ… `AuthService` - Middleware-based authentication
24
- - βœ… `DBService` - QueryService pattern for data access
25
- - βœ… `BackupService` - Async background uploads
26
- - βœ… `DriveService` - Google Drive integration
27
- - βœ… `EncryptionService` - Crypto utilities
28
- - βœ… `RazorpayService` - Payment gateway integration
29
- - βœ… `EmailService` - Email sending
30
- - βœ… `GeminiService` - AI service integration
31
-
32
- ---
33
-
34
- ## πŸ” Issues Found
35
-
36
- ### CRITICAL: Audit Logging Inconsistency
37
-
38
- **Location:** `routers/blink.py` (Lines 547, 563)
39
-
40
- **Problem:**
41
- ```python
42
- # Direct AuditLog creation in router
43
- audit_log = AuditLog(
44
- log_type="client",
45
- user_id=server_user_id,
46
- ...
47
- )
48
- db.add(audit_log)
49
- ```
50
-
51
- **Why It's Bad:**
52
- - Bypasses `AuditService.log_event()`
53
- - Inconsistent with other audit logging
54
- - No centralized audit logic
55
- - Manual field population (error-prone)
56
-
57
- **Recommendation:** Refactor to use `AuditService`
58
-
59
- ---
60
-
61
- ## πŸ“‹ Refactoring Opportunities
62
-
63
- ### 1. **Audit Service β†’ Audit Middleware** (RECOMMENDED)
64
-
65
- **Current State:**
66
- - `AuditService.log_event()` called manually in endpoints
67
- - Some endpoints create `AuditLog` directly (`blink.py`)
68
- - Inconsistent usage across codebase
69
-
70
- **Proposed Refactoring:**
71
- Create `AuditMiddleware` similar to `CreditMiddleware`:
72
-
73
- ```python
74
- class AuditMiddleware:
75
- """Automatically log all requests/responses."""
76
-
77
- async def dispatch(self, request, call_next):
78
- # Capture request details
79
- start_time = time.time()
80
-
81
- # Process request
82
- response = await call_next(request)
83
-
84
- # Automatically log based on response
85
- duration = time.time() - start_time
86
- await self.log_request(request, response, duration)
87
-
88
- return response
89
- ```
90
-
91
- **Benefits:**
92
- - βœ… Every request automatically logged
93
- - βœ… Consistent audit trail
94
- - βœ… Routers unaware of audit logging
95
- - βœ… Centralized logging logic
96
- - βœ… No manual logging calls needed
97
-
98
- **Effort:** Medium (2-3 days)
99
-
100
- ---
101
-
102
- ### 2. **Email Service β†’ Email Queue** (OPTIONAL)
103
-
104
- **Current State:**
105
- - `EmailService` sends emails synchronously
106
- - Endpoint waits for email to send
107
- - No retry mechanism
108
- - No email queue
109
-
110
- **Proposed Refactoring:**
111
- Add background email queue:
112
-
113
- ```python
114
- # Instead of:
115
- await EmailService.send_email(...)
116
-
117
- # Use:
118
- await EmailQueue.enqueue(
119
- to="user@example.com",
120
- subject="...",
121
- body="..."
122
- )
123
- # Returns immediately, email sent in background
124
- ```
125
-
126
- **Benefits:**
127
- - βœ… Faster API responses
128
- - βœ… Automatic retries on failure
129
- - βœ… Email history/tracking
130
- - βœ… Rate limiting built-in
131
-
132
- **Effort:** High (4-5 days)
133
-
134
- ---
135
-
136
- ### 3. **API Key Manager β†’ API Key Middleware** (OPTIONAL)
137
-
138
- **Current State:**
139
- - `api_key_manager.py` manages Gemini API keys
140
- - Manual rotation logic
141
- - No automatic rate limiting per key
142
-
143
- **Proposed Refactoring:**
144
- Add middleware for automatic key rotation and rate limiting:
145
-
146
- ```python
147
- class APIKeyMiddleware:
148
- """Automatically select and rotate API keys."""
149
-
150
- async def dispatch(self, request, call_next):
151
- if self.is_gemini_request(request):
152
- api_key = await self.get_available_key()
153
- request.state.gemini_api_key = api_key
154
-
155
- response = await call_next(request)
156
-
157
- # Track usage
158
- await self.track_key_usage(api_key, response)
159
-
160
- return response
161
- ```
162
-
163
- **Benefits:**
164
- - βœ… Automatic key selection
165
- - βœ… Per-key rate limiting
166
- - βœ… Automatic rotation on errors
167
- - βœ… Usage analytics
168
-
169
- **Effort:** Medium (3-4 days)
170
-
171
- ---
172
-
173
- ### 4. **Payment Transaction Tracking** (NICE TO HAVE)
174
-
175
- **Current State:**
176
- - `PaymentTransaction` model exists
177
- - Records created in `routers/payments.py`
178
- - No transaction history endpoint
179
- - No failed payment tracking
180
-
181
- **Proposed Enhancement:**
182
- Add `PaymentTransactionManager` similar to `CreditTransactionManager`:
183
-
184
- ```python
185
- class PaymentTransactionManager:
186
- """Centralized payment transaction management."""
187
-
188
- @staticmethod
189
- async def create_order(...):
190
- # Create payment transaction
191
- # Record attempt
192
- # Return order details
193
-
194
- @staticmethod
195
- async def verify_payment(...):
196
- # Verify signature
197
- # Update transaction
198
- # Trigger credit addition
199
- ```
200
-
201
- **Benefits:**
202
- - βœ… Centralized payment logic
203
- - βœ… Better error tracking
204
- - βœ… Payment analytics
205
- - βœ… Audit trail
206
-
207
- **Effort:** Low (1-2 days)
208
-
209
- ---
210
-
211
- ## 🎯 Recommended Action Plan
212
-
213
- ### Phase 1: Critical Fixes (Do Now)
214
- 1. **Fix `blink.py` audit logging** - Use `AuditService` instead of direct `AuditLog` creation
215
- - **Effort:** 1 hour
216
- - **Priority:** HIGH
217
- - **Impact:** Consistency, maintainability
218
-
219
- ### Phase 2: High-Value Refactorings (Do Soon)
220
- 2. **Implement Audit Middleware**
221
- - **Effort:** 2-3 days
222
- - **Priority:** MEDIUM-HIGH
223
- - **Impact:** Automatic request logging, better audit trail
224
-
225
- 3. **Implement Job Deletion Credit Refunds**
226
- - **Effort:** 4 hours (already has TODO)
227
- - **Priority:** MEDIUM
228
- - **Impact:** Complete credit service feature set
229
-
230
- ### Phase 3: Optional Enhancements (Do Later)
231
- 4. **Email Queue System**
232
- - **Effort:** 4-5 days
233
- - **Priority:** LOW-MEDIUM
234
- - **Impact:** Better UX, reliability
235
-
236
- 5. **Payment Transaction Manager**
237
- - **Effort:** 1-2 days
238
- - **Priority:** LOW
239
- - **Impact:** Better analytics, tracking
240
-
241
- 6. **API Key Middleware**
242
- - **Effort:** 3-4 days
243
- - **Priority:** LOW
244
- - **Impact:** Better key management
245
-
246
- ---
247
-
248
- ## πŸ“Š Service Health Summary
249
-
250
- | Service | Status | Architecture | Notes |
251
- |---------|--------|--------------|-------|
252
- | CreditService | βœ… Excellent | Middleware-driven | Recently refactored, production-ready |
253
- | AuthService | βœ… Good | Middleware-based | Clean, well-tested |
254
- | DBService | βœ… Good | QueryService pattern | Consistent usage |
255
- | AuditService | ⚠️ Needs Work | Mixed (service + direct) | Inconsistent usage in blink.py |
256
- | BackupService | βœ… Good | Background async | Works well |
257
- | DriveService | βœ… Good | Utility service | Simple, effective |
258
- | EmailService | ⚠️ Could Improve | Synchronous | No queue, no retries |
259
- | RazorpayService | βœ… Good | Integration wrapper | Clean interface |
260
- | GeminiService | βœ… Good | Job queue pattern | Well-structured |
261
- | APIKeyManager | ⚠️ Could Improve | Manual management | No middleware integration |
262
-
263
- ---
264
-
265
- ## πŸ”§ Technical Debt Items
266
-
267
- ### Found During Audit
268
-
269
- 1. **`blink.py` audit logging** - Direct `AuditLog` creation
270
- 2. **Job deletion refunds** - TODO comment in `gemini.py:564`
271
- 3. **Email sending** - No queue or async processing
272
- 4. **API key rotation** - Manual, not middleware-driven
273
- 5. **Payment analytics** - No aggregated stats endpoints
274
-
275
- ---
276
-
277
- ## πŸ’‘ Key Learnings from Credit Service Refactoring
278
-
279
- **What Worked Well:**
280
- 1. βœ… Middleware pattern for cross-cutting concerns
281
- 2. βœ… Transaction manager for centralized logic
282
- 3. βœ… Response inspection for automatic actions
283
- 4. βœ… Complete audit trail via transaction table
284
- 5. βœ… Zero application awareness
285
-
286
- **Apply These Patterns To:**
287
- - Audit logging (middleware)
288
- - Email sending (queue + manager)
289
- - API key management (middleware)
290
- - Payment tracking (transaction manager)
291
-
292
- ---
293
-
294
- ## πŸš€ Next Steps
295
-
296
- ### Immediate (This Week)
297
- 1. Fix `blink.py` to use `AuditService`
298
- 2. Implement job deletion credit refunds (TODO)
299
- 3. Add tests for audit service
300
-
301
- ### Short-Term (This Month)
302
- 4. Design and implement `AuditMiddleware`
303
- 5. Add payment transaction history endpoint
304
- 6. Improve error handling across services
305
-
306
- ### Long-Term (Next Quarter)
307
- 7. Implement email queue system
308
- 8. Add API key middleware
309
- 9. Build service health monitoring dashboard
310
-
311
- ---
312
-
313
- ## πŸ“ˆ Conclusion
314
-
315
- **Overall Codebase Health: 8.5/10**
316
-
317
- The codebase is generally well-structured with good separation of concerns. The recent credit service refactoring demonstrates a strong pattern that can be applied to other services.
318
-
319
- **Main Strengths:**
320
- - Clean router layer
321
- - Middleware-centric architecture
322
- - Good service patterns
323
- - Comprehensive testing
324
-
325
- **Areas for Improvement:**
326
- - Audit logging consistency
327
- - Async processing (emails)
328
- - Service middleware adoption
329
-
330
- **Recommended Focus:**
331
- Start with Phase 1 critical fixes, then evaluate Phase 2 based on business priorities.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
CREDIT_AUDIT_REPORT.md DELETED
@@ -1,169 +0,0 @@
1
- # Credit Code Audit Report
2
-
3
- ## Executive Summary
4
-
5
- Completed full codebase audit for credit operations. **All legacy credit code has been removed or deprecated.**
6
-
7
- All credit operations now flow through one of two paths:
8
- 1. **Middleware** β†’ `CreditTransactionManager` (for API endpoints)
9
- 2. **Payment Router** β†’ `CreditTransactionManager` (for purchases)
10
-
11
- ---
12
-
13
- ## Changes Made
14
-
15
- ### βœ… 1. Deprecated Legacy Credit Dependencies (`dependencies.py`)
16
-
17
- **REMOVED:**
18
- - `verify_credits()` - Manually deducted 1 credit
19
- - `verify_video_credits()` - Manually deducted 10 credits
20
-
21
- **STATUS:** Commented out with deprecation notice
22
-
23
- These functions directly manipulated `user.credits` without creating transaction records, bypassing the audit trail.
24
-
25
- ---
26
-
27
- ### βœ… 2. Removed Manual Refunds (`routers/gemini.py`)
28
-
29
- **REMOVED:**
30
- - Lines 572, 583: Manual `user.credits +=` in `delete_job` endpoint
31
- - Complex refund logic based on job status
32
-
33
- **REASON:**
34
- - Job deletion refunds should be handled by `CreditTransactionManager.refund_credits()` for proper tracking
35
- - Added TODO comment for future implementation
36
-
37
- **IMPACT:**
38
- - `DELETE /gemini/job/{job_id}` now returns `refund_amount: 0`
39
- - Soft deletion still works, just no automatic refunds yet
40
- - Can be re-implemented properly using transaction manager later
41
-
42
- ---
43
-
44
- ### βœ… 3. Updated Comments (`routers/gemini.py`)
45
-
46
- **CHANGED:**
47
- ```python
48
- # Old comment
49
- # Authentication is handled by dependencies.verify_credits
50
-
51
- # New comment
52
- # Authentication and credit handling is managed automatically by middleware.
53
- ```
54
-
55
- ---
56
-
57
- ## Credit Operations Inventory
58
-
59
- ### βœ… GOOD: Using CreditTransactionManager
60
-
61
- 1. **Middleware** (`services/credit_service/middleware.py`)
62
- - `CreditTransactionManager.reserve_credits()`
63
- - `CreditTransactionManager.confirm_credits()`
64
- - `CreditTransactionManager.refund_credits()`
65
-
66
- 2. **Payment Router** (`routers/payments.py`)
67
- - `CreditTransactionManager.add_credits()`
68
-
69
- 3. **Transaction Manager** (`services/credit_service/transaction_manager.py`)
70
- - `user.credits +=` (Line 272, 329) - βœ… OK (within transaction manager)
71
- - `user.credits -=` (Line 129) - βœ… OK (within transaction manager)
72
-
73
- ### βœ… LEGACY BUT ACCEPTABLE: Job Lifecycle Management
74
-
75
- 4. **Credit Manager** (`services/credit_service/credit_manager.py`)
76
- - `user.credits -=` (Line 113) - βœ… OK (`reserve_credit` for job workers)
77
- - `user.credits +=` (Line 175) - βœ… OK (`refund_credit` for job workers)
78
-
79
- **WHY ACCEPTABLE:**
80
- - These are called by background workers, not API endpoints
81
- - Used for job lifecycle management (queued β†’ processing β†’ completed/failed)
82
- - Part of the existing system that works alongside middleware
83
- - Middleware handles initial reservation, credit_manager handles job completion refunds
84
-
85
- ### ❌ REMOVED: Direct API Endpoint Manipulation
86
-
87
- 5. **Dependencies** (`dependencies.py`) - DEPRECATED βœ…
88
- 6. **Gemini Router** (`routers/gemini.py`) - REMOVED βœ…
89
-
90
- ---
91
-
92
- ## Test Files
93
-
94
- The following files have direct credit manipulation for TESTING purposes only:
95
- - `tests/test_job_lifecycle.py`
96
- - `tests/test_worker_pool.py`
97
-
98
- **ACTION:** None needed - test files can manipulate credits directly for test scenarios
99
-
100
- ---
101
-
102
- ## Final Architecture
103
-
104
- ```
105
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
106
- β”‚ USER REQUEST β”‚
107
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
108
- β”‚
109
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
110
- β”‚ β”‚
111
- API Endpoints Payment Endpoints
112
- β”‚ β”‚
113
- β–Ό β–Ό
114
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
115
- β”‚ Middleware β”‚ β”‚ Payment β”‚
116
- β”‚ Handles β”‚ β”‚ Router β”‚
117
- β”‚ Credits β”‚ β”‚ β”‚
118
- β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
119
- β”‚ β”‚
120
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
121
- └────►CreditTransactionβ—„β”˜
122
- β”‚ Manager β”‚
123
- β”‚(Single Source of β”‚
124
- β”‚ Truth) β”‚
125
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
126
- β”‚
127
- β–Ό
128
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
129
- β”‚ credit_ β”‚
130
- β”‚ transactions β”‚
131
- β”‚ table β”‚
132
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
133
- ```
134
-
135
- All credit database operations go through `CreditTransactionManager` βœ…
136
-
137
- ---
138
-
139
- ## Verification Checklist
140
-
141
- - [x] No `Depends(verify_credits)` in any router
142
- - [x] No `Depends(verify_video_credits)` in any router
143
- - [x] No direct `user.credits +=` in routers (except legacy code commented)
144
- - [x] No direct `user.credits -=` in routers (except legacy code commented)
145
- - [x] Middleware uses `CreditTransactionManager`
146
- - [x] Payment router uses `CreditTransactionManager`
147
- - [x] All credit changes create transaction records
148
- - [x] Complete audit trail available
149
-
150
- ---
151
-
152
- ## Remaining Work (Optional)
153
-
154
- 1. **Job Deletion Refunds** - Re-implement using `CreditTransactionManager`
155
- 2. **Delete Legacy Code** - After confirming migration works, delete commented functions
156
- 3. **Async Job Credit Handling** - Ensure middleware properly handles job status checks
157
- 4. **Test Suite** - Add tests for new transaction-based credit system
158
-
159
- ---
160
-
161
- ## Conclusion
162
-
163
- βœ… **All legacy credit misuse has been eliminated.**
164
- βœ… **Single source of truth: `CreditTransactionManager`**
165
- βœ… **Complete audit trail for all operations**
166
- βœ… **Middleware handles API credits automatically**
167
- βœ… **Payment router properly tracks purchases**
168
-
169
- The codebase is now clean and follows the centralized credit management architecture.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
FINAL_ARCHITECTURE_AUDIT.md DELETED
@@ -1,294 +0,0 @@
1
- # Final Codebase Architecture Audit Report
2
-
3
- ## 🎯 Executive Summary
4
-
5
- **Status:** βœ… **EXCELLENT** - Codebase is in great shape!
6
-
7
- After comprehensive analysis, the architecture is **production-ready** with no critical issues remaining.
8
-
9
- ---
10
-
11
- ## βœ… What's Perfect
12
-
13
- ### 1. **Routers Are Completely Clean**
14
- - ❌ No `db.commit()` calls in routers
15
- - ❌ No `session.add()` calls in routers
16
- - ❌ No `AuditLog()` direct creation
17
- - ❌ No `user.credits +=` manual operations
18
- - βœ… All routers use services/managers properly
19
-
20
- ### 2. **Middleware Stack is Complete**
21
- ```
22
- Auth β†’ Audit β†’ API Key β†’ Credit β†’ Application
23
- ```
24
- All cross-cutting concerns handled automatically!
25
-
26
- ### 3. **Service Architecture is Consistent**
27
-
28
- | Service | Type | Status |
29
- |---------|------|--------|
30
- | **Auth Service** | Middleware | βœ… Perfect |
31
- | **Audit Service** | Middleware | βœ… Perfect |
32
- | **API Key Service** | Middleware | βœ… Perfect |
33
- | **Credit Service** | Middleware + Manager | βœ… Perfect |
34
- | **Payment Service** | Transaction Manager | βœ… Perfect |
35
- | **DB Service** | QueryService Pattern | βœ… Perfect |
36
- | **Backup Service** | Async Service | βœ… Perfect |
37
- | **Drive Service** | Utility Service | βœ… Good |
38
- | **Email Service** | Utility Service | βœ… Good |
39
- | **Encryption Service** | Utility Service | βœ… Good |
40
- | **Razorpay Service** | Integration Service | βœ… Good |
41
-
42
- ---
43
-
44
- ## πŸ’‘ Optional Minor Enhancements
45
-
46
- These are **NOT required** but could add value in the future:
47
-
48
- ### 1. Email Queue System (Low Priority)
49
-
50
- **Current State:**
51
- ```python
52
- # Email is sent synchronously
53
- await send_email(to_email, subject, body)
54
- # Endpoint waits for email to send
55
- ```
56
-
57
- **Enhancement:**
58
- ```python
59
- # Email queue for async processing
60
- await EmailQueue.enqueue(to_email, subject, body)
61
- # Returns immediately, email sent in background
62
- ```
63
-
64
- **Benefits:**
65
- - Faster API responses
66
- - Automatic retries on failure
67
- - Email history tracking
68
- - Rate limiting
69
-
70
- **Effort:** 1-2 days
71
- **Priority:** LOW (only if sending many emails)
72
-
73
- ---
74
-
75
- ### 2. Job Deletion Credit Refunds (Medium Priority)
76
-
77
- **Current State:**
78
- ```python
79
- # DELETE /gemini/job/{job_id}
80
- # TODO: Implement credit refund via CreditTransactionManager
81
- ```
82
-
83
- **Enhancement:**
84
- Implement proper refund logic using `CreditTransactionManager.refund_credits()`
85
-
86
- **Benefits:**
87
- - Fair refunds for cancelled jobs
88
- - Uses existing transaction manager
89
- - Complete audit trail
90
-
91
- **Effort:** 2-3 hours
92
- **Priority:** MEDIUM (if users delete jobs frequently)
93
-
94
- ---
95
-
96
- ### 3. API Key Health Dashboard (Nice to Have)
97
-
98
- **Enhancement:**
99
- ```
100
- GET /admin/api-keys/health
101
- {
102
- "total_keys": 3,
103
- "healthy_keys": 2,
104
- "keys_in_cooldown": 1,
105
- "quota_remaining": "55%"
106
- }
107
- ```
108
-
109
- **Benefits:**
110
- - Monitor key health
111
- - Proactive quota management
112
- - Better debugging
113
-
114
- **Effort:** 3-4 hours
115
- **Priority:** LOW (nice-to-have for monitoring)
116
-
117
- ---
118
-
119
- ## πŸ” Detailed Audit Findings
120
-
121
- ### Services Analyzed
122
-
123
- **1. Email Service** (`services/email_service.py`)
124
- - βœ… Clean implementation
125
- - βœ… Gmail API + SMTP fallback
126
- - ℹ️ Synchronous (acceptable for current use)
127
- - πŸ’‘ Could add email queue (optional)
128
-
129
- **2. Backup Service** (`services/backup_service/`)
130
- - βœ… Async implementation
131
- - βœ… Debouncing to prevent excessive uploads
132
- - βœ… Lock mechanism to prevent concurrent uploads
133
- - βœ… Force option for critical backups
134
- - βœ… **Perfect!**
135
-
136
- **3. Drive Service** (`services/drive_service.py`)
137
- - βœ… Google Drive integration
138
- - βœ… Upload/download functionality
139
- - βœ… Clean implementation
140
- - βœ… **No changes needed**
141
-
142
- **4. Encryption Service** (`services/encryption_service.py`)
143
- - βœ… Crypto utilities
144
- - βœ… Clean implementation
145
- - βœ… **No changes needed**
146
-
147
- **5. Razorpay Service** (`services/razorpay_service.py`)
148
- - βœ… Payment gateway integration
149
- - βœ… Signature verification
150
- - βœ… Works with PaymentTransactionManager
151
- - βœ… **No changes needed**
152
-
153
- **6. API Key Manager** (`services/api_key_manager.py`)
154
- - βœ… Usage tracking
155
- - βœ… Least-used selection
156
- - βœ… Works with APIKeyMiddleware
157
- - βœ… **No changes needed**
158
-
159
- ---
160
-
161
- ## πŸ“Š Code Quality Metrics
162
-
163
- ### Architecture Patterns
164
-
165
- | Pattern | Count | Status |
166
- |---------|-------|--------|
167
- | **Middleware** | 4 | βœ… Excellent |
168
- | **Transaction Managers** | 2 | βœ… Excellent |
169
- | **Service Classes** | 10+ | βœ… Good |
170
- | **Direct DB Operations in Routers** | 0 | βœ… Perfect |
171
- | **Manual Credit Operations** | 0 | βœ… Perfect |
172
- | **Direct Audit Log Creation** | 0 | βœ… Perfect |
173
-
174
- ### Test Coverage
175
-
176
- | Component | Tests | Coverage |
177
- |-----------|-------|----------|
178
- | Credit Service | βœ… 55+ tests | 90%+ |
179
- | Other Services | ⚠️ Minimal | TBD |
180
-
181
- **Recommendation:** Add tests for audit, API key, and payment services (future work)
182
-
183
- ---
184
-
185
- ## 🎯 Current Architecture Diagram
186
-
187
- ```
188
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
189
- β”‚ USER REQUEST β”‚
190
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
191
- β”‚
192
- β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
193
- β”‚ CORS Middlewareβ”‚
194
- β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
195
- β”‚
196
- β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
197
- β”‚ Auth Middleware β”‚ ← Authenticate user
198
- β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
199
- β”‚
200
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
201
- β”‚ Audit Middleware β”‚ ← Log request/response
202
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
203
- β”‚
204
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
205
- β”‚ API Key Middleware β”‚ ← Select Gemini key
206
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
207
- β”‚
208
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
209
- β”‚ Credit Middleware β”‚ ← Reserve/confirm credits
210
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
211
- β”‚
212
- β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
213
- β”‚ Application β”‚ ← Business logic
214
- β”‚ Endpoints β”‚
215
- β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
216
- β”‚
217
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
218
- β”‚ Service Layer β”‚
219
- β”‚ - CreditTransactionMgr β”‚
220
- β”‚ - PaymentTransactionMgrβ”‚
221
- β”‚ - QueryService β”‚
222
- β”‚ - BackupService β”‚
223
- β”‚ - EmailService β”‚
224
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
225
- β”‚
226
- β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
227
- β”‚ Database β”‚
228
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
229
- ```
230
-
231
- ---
232
-
233
- ## βœ… Final Recommendations
234
-
235
- ### Do Now (Critical): **NONE!** πŸŽ‰
236
- Everything critical is complete and working!
237
-
238
- ### Do Soon (High Value):
239
- 1. **Job Deletion Refunds** (2-3 hours)
240
- - Implement using `CreditTransactionManager.refund_credits()`
241
- - Complete the TODO in `gemini.py:564`
242
-
243
- ### Do Later (Nice to Have):
244
- 2. **Email Queue** (1-2 days)
245
- - Only if sending many emails
246
- - Current sync approach works fine for low volume
247
-
248
- 3. **API Key Health Endpoint** (3-4 hours)
249
- - Monitoring dashboard
250
- - Not essential but nice for ops
251
-
252
- 4. **Test Suite Expansion** (3-5 days)
253
- - Add tests for audit middleware
254
- - Add tests for API key middleware
255
- - Add tests for payment manager
256
-
257
- ---
258
-
259
- ## πŸ† Achievement Summary
260
-
261
- **What We Accomplished:**
262
-
263
- 1. βœ… **Credit System** - Fully middleware-driven with complete audit trail
264
- 2. βœ… **Audit Logging** - Automatic request/response logging
265
- 3. βœ… **API Key Management** - Smart rotation with quota recovery
266
- 4. βœ… **Payment System** - Centralized transaction management + analytics
267
- 5. βœ… **Clean Architecture** - Zero direct DB operations in routers
268
- 6. βœ… **Consistent Patterns** - Middleware + transaction managers
269
-
270
- **Code Quality:**
271
- - **Clean Routers** - 100% service-driven
272
- - **Middleware Stack** - 4 layers of automatic processing
273
- - **Transaction Managers** - 2 centralized managers
274
- - **Test Coverage** - 90%+ for credit service
275
- - **Zero Tech Debt** - No critical issues remaining
276
-
277
- ---
278
-
279
- ## πŸŽ‰ Conclusion
280
-
281
- **The codebase is production-ready!**
282
-
283
- **Architecture Grade: A+**
284
-
285
- All critical refactoring complete. The optional enhancements are truly optional - the system works great as-is.
286
-
287
- **Next Steps:**
288
- 1. βœ… Code is deployed (main branch)
289
- 2. βœ… All 4 phases complete
290
- 3. ⏳ **Test in production**
291
- 4. ⏳ **Monitor performance**
292
- 5. ⏳ **Consider optional enhancements based on usage**
293
-
294
- **Congratulations! 🎊**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,8 +1,5 @@
1
  """
2
- FastAPI URL Blink Application
3
-
4
- Production-grade API for receiving encrypted user data,
5
- decrypting it, and storing in SQLite database.
6
  """
7
  import os
8
  import logging
@@ -29,10 +26,7 @@ drive_service = DriveService()
29
 
30
  @asynccontextmanager
31
  async def lifespan(app: FastAPI):
32
- """
33
- Application lifespan manager.
34
- Initializes database on startup.
35
- """
36
  logger.info("Starting up - initializing database...")
37
 
38
  # Register DB Service configuration
@@ -41,8 +35,6 @@ async def lifespan(app: FastAPI):
41
 
42
  # Initialize Backup Service
43
  from services.backup_service import initialize_backup_service
44
- # Note: drive_service is already initialized globally, but re-initialized here as per instruction.
45
- # The global drive_service is used later for download/upload.
46
  local_drive_service = DriveService()
47
  backup_service = initialize_backup_service(
48
  local_drive_service,
@@ -109,7 +101,7 @@ async def lifespan(app: FastAPI):
109
  "cost": 10,
110
  "type": "async"
111
  },
112
- # Status check endpoints - inspect response for job completion
113
  "/gemini/job/{job_id}": {
114
  "cost": 0, # No additional cost for status checks
115
  "type": "async"
@@ -191,36 +183,35 @@ app = FastAPI(
191
  lifespan=lifespan
192
  )
193
 
194
- # Configure CORS
195
- # For cookies to work with credentials, we need specific origins (not "*")
196
  allowed_origins = os.getenv("CORS_ORIGINS", "http://localhost:3000,http://localhost:5173").split(",")
197
  logger.info(f"CORS allowed origins: {allowed_origins}")
198
 
199
  app.add_middleware(
200
  CORSMiddleware,
201
- allow_origins=allowed_origins, # Specific origins for production
202
  allow_credentials=True,
203
  allow_methods=["*"],
204
  allow_headers=["*"],
205
  )
206
 
207
- # Add Audit Middleware (executes second - after auth, before API key)
208
  from services.audit_service import AuditMiddleware
209
  app.add_middleware(AuditMiddleware)
210
 
211
- # Add API Key Middleware (executes third - for Gemini requests)
212
  from services.gemini_service import APIKeyMiddleware
213
  app.add_middleware(APIKeyMiddleware)
214
 
215
- # Add Credit Middleware (executes fourth - after auth, audit, and API key)
216
  from services.credit_service import CreditMiddleware
217
  app.add_middleware(CreditMiddleware)
218
 
219
- # Add Auth Middleware second (executes first - sets user)
220
  from services.auth_service import AuthMiddleware
221
  app.add_middleware(AuthMiddleware)
222
 
223
- # Include Routers
224
  app.include_router(general.router)
225
  app.include_router(auth.router)
226
  app.include_router(blink.router)
@@ -233,9 +224,7 @@ app.include_router(schema.router)
233
 
234
  @app.exception_handler(Exception)
235
  async def global_exception_handler(request: Request, exc: Exception):
236
- """
237
- Global exception handler for unhandled errors.
238
- """
239
  logger.error(f"Unhandled exception: {exc}")
240
  return JSONResponse(
241
  status_code=500,
 
1
  """
2
+ FastAPI Application - API Gateway with credit management and AI services.
 
 
 
3
  """
4
  import os
5
  import logging
 
26
 
27
  @asynccontextmanager
28
  async def lifespan(app: FastAPI):
29
+ """Application lifespan manager."""
 
 
 
30
  logger.info("Starting up - initializing database...")
31
 
32
  # Register DB Service configuration
 
35
 
36
  # Initialize Backup Service
37
  from services.backup_service import initialize_backup_service
 
 
38
  local_drive_service = DriveService()
39
  backup_service = initialize_backup_service(
40
  local_drive_service,
 
101
  "cost": 10,
102
  "type": "async"
103
  },
104
+
105
  "/gemini/job/{job_id}": {
106
  "cost": 0, # No additional cost for status checks
107
  "type": "async"
 
183
  lifespan=lifespan
184
  )
185
 
186
+
 
187
  allowed_origins = os.getenv("CORS_ORIGINS", "http://localhost:3000,http://localhost:5173").split(",")
188
  logger.info(f"CORS allowed origins: {allowed_origins}")
189
 
190
  app.add_middleware(
191
  CORSMiddleware,
192
+ allow_origins=allowed_origins,
193
  allow_credentials=True,
194
  allow_methods=["*"],
195
  allow_headers=["*"],
196
  )
197
 
198
+
199
  from services.audit_service import AuditMiddleware
200
  app.add_middleware(AuditMiddleware)
201
 
202
+
203
  from services.gemini_service import APIKeyMiddleware
204
  app.add_middleware(APIKeyMiddleware)
205
 
206
+
207
  from services.credit_service import CreditMiddleware
208
  app.add_middleware(CreditMiddleware)
209
 
210
+
211
  from services.auth_service import AuthMiddleware
212
  app.add_middleware(AuthMiddleware)
213
 
214
+
215
  app.include_router(general.router)
216
  app.include_router(auth.router)
217
  app.include_router(blink.router)
 
224
 
225
  @app.exception_handler(Exception)
226
  async def global_exception_handler(request: Request, exc: Exception):
227
+ """Global exception handler."""
 
 
228
  logger.error(f"Unhandled exception: {exc}")
229
  return JSONResponse(
230
  status_code=500,
docs/CLIENT_INTEGRATION.md DELETED
@@ -1,287 +0,0 @@
1
- # Google Sign-In Client Integration Guide
2
-
3
- Quick guide for frontend developers to integrate Google Sign-In with the APIGateway.
4
-
5
- ## Setup
6
-
7
- ### 1. Get Your Google Client ID
8
-
9
- Use the same `GOOGLE_CLIENT_ID` that's configured on the backend.
10
-
11
- ### 2. Add Google Identity Services Script
12
-
13
- ```html
14
- <script src="https://accounts.google.com/gsi/client" async defer></script>
15
- ```
16
-
17
- ---
18
-
19
- ## Option A: One-Tap / Button Sign-In (Recommended)
20
-
21
- ```html
22
- <!DOCTYPE html>
23
- <html>
24
- <head>
25
- <script src="https://accounts.google.com/gsi/client" async defer></script>
26
- </head>
27
- <body>
28
- <!-- Google One Tap prompt -->
29
- <div id="g_id_onload"
30
- data-client_id="YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com"
31
- data-callback="handleGoogleSignIn"
32
- data-auto_prompt="false">
33
- </div>
34
-
35
- <!-- Sign In Button -->
36
- <div class="g_id_signin"
37
- data-type="standard"
38
- data-size="large"
39
- data-theme="outline"
40
- data-text="sign_in_with"
41
- data-shape="rectangular"
42
- data-logo_alignment="left">
43
- </div>
44
-
45
- <script>
46
- const API_BASE = 'https://your-api-gateway.com'; // Change this
47
-
48
- async function handleGoogleSignIn(response) {
49
- try {
50
- // Send Google token to your backend
51
- const res = await fetch(`${API_BASE}/auth/google`, {
52
- method: 'POST',
53
- headers: { 'Content-Type': 'application/json' },
54
- body: JSON.stringify({
55
- id_token: response.credential,
56
- temp_user_id: localStorage.getItem('temp_user_id') // optional
57
- })
58
- });
59
-
60
- const data = await res.json();
61
-
62
- if (data.success) {
63
- // Store the access token
64
- localStorage.setItem('access_token', data.access_token);
65
- localStorage.setItem('user', JSON.stringify({
66
- user_id: data.user_id,
67
- email: data.email,
68
- name: data.name,
69
- credits: data.credits
70
- }));
71
-
72
- console.log('Signed in!', data.is_new_user ? 'New user' : 'Existing user');
73
- // Redirect or update UI
74
- window.location.reload();
75
- } else {
76
- alert('Sign in failed: ' + (data.detail || 'Unknown error'));
77
- }
78
- } catch (error) {
79
- console.error('Sign in error:', error);
80
- alert('Sign in failed. Please try again.');
81
- }
82
- }
83
- </script>
84
- </body>
85
- </html>
86
- ```
87
-
88
- ---
89
-
90
- ## Option B: Programmatic Sign-In
91
-
92
- ```javascript
93
- const API_BASE = 'https://your-api-gateway.com';
94
-
95
- // Initialize Google Sign-In
96
- function initGoogleSignIn(clientId) {
97
- google.accounts.id.initialize({
98
- client_id: clientId,
99
- callback: handleGoogleSignIn
100
- });
101
-
102
- // Render button in a container
103
- google.accounts.id.renderButton(
104
- document.getElementById('google-signin-btn'),
105
- { theme: 'outline', size: 'large', text: 'signin_with' }
106
- );
107
- }
108
-
109
- // Handle the response
110
- async function handleGoogleSignIn(response) {
111
- const res = await fetch(`${API_BASE}/auth/google`, {
112
- method: 'POST',
113
- headers: { 'Content-Type': 'application/json' },
114
- body: JSON.stringify({ id_token: response.credential })
115
- });
116
-
117
- const data = await res.json();
118
- if (data.success) {
119
- localStorage.setItem('access_token', data.access_token);
120
- }
121
- }
122
-
123
- // Call on page load
124
- window.onload = () => initGoogleSignIn('YOUR_CLIENT_ID.apps.googleusercontent.com');
125
- ```
126
-
127
- ---
128
-
129
- ## Making API Calls
130
-
131
- After sign-in, use the access token for all API calls:
132
-
133
- ```javascript
134
- const API_BASE = 'https://your-api-gateway.com';
135
-
136
- async function apiCall(endpoint, options = {}) {
137
- const token = localStorage.getItem('access_token');
138
-
139
- if (!token) {
140
- throw new Error('Not signed in');
141
- }
142
-
143
- const response = await fetch(`${API_BASE}${endpoint}`, {
144
- ...options,
145
- headers: {
146
- 'Authorization': `Bearer ${token}`,
147
- 'Content-Type': 'application/json',
148
- ...options.headers
149
- }
150
- });
151
-
152
- if (response.status === 401) {
153
- // Token expired - need to sign in again
154
- localStorage.removeItem('access_token');
155
- window.location.href = '/login';
156
- return;
157
- }
158
-
159
- return response.json();
160
- }
161
-
162
- // Examples
163
- async function getCurrentUser() {
164
- return apiCall('/auth/me');
165
- }
166
-
167
- async function generateText(prompt) {
168
- return apiCall('/gemini/generate-text', {
169
- method: 'POST',
170
- body: JSON.stringify({ prompt })
171
- });
172
- }
173
-
174
- async function checkJobStatus(jobId) {
175
- return apiCall(`/gemini/job/${jobId}`);
176
- }
177
- ```
178
-
179
- ---
180
-
181
- ## API Endpoints Reference
182
-
183
- | Endpoint | Method | Auth Required | Description |
184
- |----------|--------|---------------|-------------|
185
- | `/auth/google` | POST | ❌ | Sign in with Google token |
186
- | `/auth/me` | GET | βœ… | Get current user info |
187
- | `/auth/refresh` | POST | ❌ | Refresh access token |
188
- | `/gemini/generate-text` | POST | βœ… | Generate text (costs 1 credit) |
189
- | `/gemini/generate-video` | POST | βœ… | Generate video (costs 1 credit) |
190
- | `/gemini/job/{job_id}` | GET | βœ… | Check job status |
191
-
192
- ---
193
-
194
- ## Sign Out
195
-
196
- ```javascript
197
- function signOut() {
198
- localStorage.removeItem('access_token');
199
- localStorage.removeItem('user');
200
-
201
- // Optionally call logout endpoint for audit
202
- fetch(`${API_BASE}/auth/logout`, {
203
- method: 'POST',
204
- headers: { 'Authorization': `Bearer ${token}` }
205
- });
206
-
207
- // Revoke Google session
208
- google.accounts.id.disableAutoSelect();
209
-
210
- window.location.href = '/';
211
- }
212
- ```
213
-
214
- ---
215
-
216
- ## Error Handling
217
-
218
- | Status | Meaning | Action |
219
- |--------|---------|--------|
220
- | 401 | Token expired/invalid | Redirect to sign-in |
221
- | 402 | Insufficient credits | Show "buy credits" prompt |
222
- | 429 | Rate limited | Wait and retry |
223
-
224
- ```javascript
225
- async function apiCallWithErrorHandling(endpoint, options) {
226
- const response = await apiCall(endpoint, options);
227
-
228
- if (response.status === 402) {
229
- alert('You have run out of credits!');
230
- return null;
231
- }
232
-
233
- return response;
234
- }
235
- ```
236
-
237
- ---
238
-
239
- ## React Example
240
-
241
- ```jsx
242
- import { useEffect, useState } from 'react';
243
-
244
- function App() {
245
- const [user, setUser] = useState(null);
246
-
247
- useEffect(() => {
248
- // Check if already signed in
249
- const token = localStorage.getItem('access_token');
250
- if (token) {
251
- fetch('/auth/me', {
252
- headers: { 'Authorization': `Bearer ${token}` }
253
- })
254
- .then(r => r.json())
255
- .then(setUser)
256
- .catch(() => localStorage.removeItem('access_token'));
257
- }
258
-
259
- // Initialize Google Sign-In
260
- window.handleGoogleSignIn = async (response) => {
261
- const res = await fetch('/auth/google', {
262
- method: 'POST',
263
- headers: { 'Content-Type': 'application/json' },
264
- body: JSON.stringify({ id_token: response.credential })
265
- });
266
- const data = await res.json();
267
- if (data.success) {
268
- localStorage.setItem('access_token', data.access_token);
269
- setUser(data);
270
- }
271
- };
272
- }, []);
273
-
274
- if (!user) {
275
- return (
276
- <div>
277
- <div id="g_id_onload"
278
- data-client_id="YOUR_CLIENT_ID"
279
- data-callback="handleGoogleSignIn" />
280
- <div className="g_id_signin" data-type="standard" />
281
- </div>
282
- );
283
- }
284
-
285
- return <div>Welcome, {user.name}! Credits: {user.credits}</div>;
286
- }
287
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/GEMINI_API.md DELETED
@@ -1,230 +0,0 @@
1
- # Gemini API - Client Integration Guide
2
-
3
- ## Authentication
4
-
5
- All endpoints require JWT authentication via Google Sign-In.
6
-
7
- ```
8
- Authorization: Bearer <your_jwt_token>
9
- ```
10
-
11
- ---
12
-
13
- ## Endpoints
14
-
15
- ### 1. Generate Text
16
- **POST** `/gemini/generate-text`
17
-
18
- ```json
19
- {
20
- "prompt": "Write a poem about the moon",
21
- "model": "gemini-2.5-flash" // optional
22
- }
23
- ```
24
-
25
- **Response:**
26
- ```json
27
- {
28
- "success": true,
29
- "job_id": "job_abc123",
30
- "status": "queued",
31
- "position": 1
32
- }
33
- ```
34
-
35
- ---
36
-
37
- ### 2. Analyze Image
38
- **POST** `/gemini/analyze-image`
39
-
40
- ```json
41
- {
42
- "base64_image": "<base64_encoded_image>",
43
- "mime_type": "image/jpeg",
44
- "prompt": "What's in this image?"
45
- }
46
- ```
47
-
48
- ---
49
-
50
- ### 3. Edit Image
51
- **POST** `/gemini/edit-image`
52
-
53
- ```json
54
- {
55
- "base64_image": "<base64_encoded_image>",
56
- "mime_type": "image/jpeg",
57
- "prompt": "Make the sky purple"
58
- }
59
- ```
60
-
61
- ---
62
-
63
- ### 4. Generate Video
64
- **POST** `/gemini/generate-video`
65
-
66
- ```json
67
- {
68
- "base64_image": "<base64_encoded_image>",
69
- "mime_type": "image/jpeg",
70
- "prompt": "Make this scene come alive with gentle motion",
71
- "aspect_ratio": "16:9", // or "9:16"
72
- "resolution": "720p", // or "1080p"
73
- "number_of_videos": 1 // 1-4
74
- }
75
- ```
76
-
77
- ---
78
-
79
- ### 5. Generate Animation Prompt
80
- **POST** `/gemini/generate-animation-prompt`
81
-
82
- ```json
83
- {
84
- "base64_image": "<base64_encoded_image>",
85
- "mime_type": "image/png",
86
- "custom_prompt": null // optional
87
- }
88
- ```
89
-
90
- ---
91
-
92
- ## Polling for Results
93
-
94
- All requests return a `job_id`. Poll for status:
95
-
96
- **GET** `/gemini/job/{job_id}`
97
-
98
- ### Status Flow:
99
- ```
100
- queued β†’ processing β†’ completed / failed
101
- ```
102
-
103
- ### Response Examples:
104
-
105
- **Queued:**
106
- ```json
107
- {
108
- "status": "queued",
109
- "position": 3
110
- }
111
- ```
112
-
113
- **Processing:**
114
- ```json
115
- {
116
- "status": "processing",
117
- "started_at": "2024-12-12T13:00:00Z"
118
- }
119
- ```
120
-
121
- **Completed (Text/Image/Analyze):**
122
- ```json
123
- {
124
- "status": "completed",
125
- "output": {
126
- "text": "Here is your poem..."
127
- }
128
- }
129
- ```
130
-
131
- **Completed (Video):**
132
- ```json
133
- {
134
- "status": "completed",
135
- "output": {
136
- "filename": "video_abc123.mp4"
137
- },
138
- "download_url": "/gemini/download/job_abc123"
139
- }
140
- ```
141
-
142
- **Failed:**
143
- ```json
144
- {
145
- "status": "failed",
146
- "error": "API rate limit exceeded"
147
- }
148
- ```
149
-
150
- ---
151
-
152
- ## Download Video
153
-
154
- **GET** `/gemini/download/{job_id}`
155
-
156
- Returns the video file directly.
157
-
158
- ---
159
-
160
- ## Cancel Job
161
-
162
- **POST** `/gemini/job/{job_id}/cancel`
163
-
164
- Only works for `queued` jobs.
165
-
166
- ```json
167
- {
168
- "success": true,
169
- "status": "cancelled"
170
- }
171
- ```
172
-
173
- ---
174
-
175
- ## Client Polling Example (JavaScript)
176
-
177
- ```javascript
178
- async function generateText(prompt) {
179
- // 1. Submit job
180
- const response = await fetch('/gemini/generate-text', {
181
- method: 'POST',
182
- headers: {
183
- 'Authorization': `Bearer ${token}`,
184
- 'Content-Type': 'application/json'
185
- },
186
- body: JSON.stringify({ prompt })
187
- });
188
- const { job_id } = await response.json();
189
-
190
- // 2. Poll until done
191
- while (true) {
192
- const status = await fetch(`/gemini/job/${job_id}`, {
193
- headers: { 'Authorization': `Bearer ${token}` }
194
- }).then(r => r.json());
195
-
196
- if (status.status === 'completed') {
197
- return status.output.text;
198
- }
199
- if (status.status === 'failed') {
200
- throw new Error(status.error);
201
- }
202
-
203
- // Wait before next poll
204
- await new Promise(r => setTimeout(r, 2000));
205
- }
206
- }
207
- ```
208
-
209
- ---
210
-
211
- ## Priority Tiers
212
-
213
- Jobs are processed with different priorities:
214
-
215
- | Job Type | Priority | Poll Interval |
216
- |----------|----------|---------------|
217
- | text, analyze, animation_prompt | Fast | ~5 sec |
218
- | image, edit_image | Medium | ~30 sec |
219
- | video | Slow | ~60 sec |
220
-
221
- ---
222
-
223
- ## Error Codes
224
-
225
- | Code | Meaning |
226
- |------|---------|
227
- | 401 | Unauthorized - Invalid/expired token |
228
- | 402 | Insufficient credits |
229
- | 404 | Job not found |
230
- | 429 | Rate limit exceeded |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
routers/gemini.py CHANGED
@@ -1,7 +1,5 @@
1
  """
2
  Gemini Router - API endpoints for Gemini AI services.
3
- Uses job queue pattern for async processing.
4
- Authentication via JWT (Authorization: Bearer <token>).
5
  """
6
  import os
7
  import uuid
@@ -20,7 +18,7 @@ from datetime import datetime
20
  router = APIRouter(prefix="/gemini", tags=["gemini"])
21
 
22
 
23
- # Request/Response Models
24
  class GenerateAnimationPromptRequest(BaseModel):
25
  base64_image: str = Field(..., description="Base64 encoded image data")
26
  mime_type: str = Field(..., description="MIME type of the image (e.g., image/png)")
@@ -53,8 +51,7 @@ class AnalyzeImageRequest(BaseModel):
53
  prompt: str = Field(..., description="Analysis prompt")
54
 
55
 
56
- # Authentication and credit handling is managed automatically by middleware.
57
- # JWT tokens via Authorization: Bearer <token> header
58
 
59
 
60
  async def get_queue_position(db: AsyncSession, job_id: str) -> int:
@@ -74,7 +71,7 @@ async def create_job(
74
  input_data: dict,
75
  credits_reserved: int = 0
76
  ) -> GeminiJob:
77
- """Create a new job in the queue with auto-assigned priority."""
78
  from services.gemini_job_worker import get_priority_for_job_type, get_pool
79
 
80
  job_id = f"job_{uuid.uuid4().hex[:16]}"
@@ -82,18 +79,18 @@ async def create_job(
82
 
83
  job = GeminiJob(
84
  job_id=job_id,
85
- user_id=user.id, # Integer FK to users.id
86
  job_type=job_type,
87
  status="queued",
88
  priority=priority,
89
  input_data=input_data,
90
- credits_reserved=credits_reserved # Track reserved credits for this job
91
  )
92
  db.add(job)
93
  await db.commit()
94
  await db.refresh(job)
95
 
96
- # Notify workers immediately so they wake up and process this job
97
  get_pool().notify_new_job(priority)
98
 
99
  return job
@@ -105,10 +102,7 @@ async def generate_animation_prompt(
105
  request: GenerateAnimationPromptRequest,
106
  db: AsyncSession = Depends(get_db)
107
  ):
108
- """
109
- Queue an animation prompt generation job.
110
- Auth and credit validation handled by middleware.
111
- """
112
  user = req.state.user
113
  credits_reserved = req.state.credits_reserved
114
  job = await create_job(
@@ -140,10 +134,7 @@ async def edit_image(
140
  request: EditImageRequest,
141
  db: AsyncSession = Depends(get_db)
142
  ):
143
- """
144
- Queue an image edit job.
145
- Auth and credit validation handled by middleware.
146
- """
147
  user = req.state.user
148
  credits_reserved = req.state.credits_reserved
149
  job = await create_job(
@@ -175,10 +166,7 @@ async def generate_video(
175
  request: GenerateVideoRequest,
176
  db: AsyncSession = Depends(get_db)
177
  ):
178
- """
179
- Queue a video generation job.
180
- Auth and credit validation handled by middleware.
181
- """
182
  user = req.state.user
183
  credits_reserved = req.state.credits_reserved
184
  job = await create_job(
@@ -193,7 +181,7 @@ async def generate_video(
193
  "resolution": request.resolution,
194
  "number_of_videos": request.number_of_videos
195
  },
196
- credits_reserved=credits_reserved # 10 credits for video
197
  )
198
 
199
  position = await get_queue_position(db, job.job_id)
@@ -213,10 +201,7 @@ async def generate_text(
213
  request: GenerateTextRequest,
214
  db: AsyncSession = Depends(get_db)
215
  ):
216
- """
217
- Queue a text generation job.
218
- Auth and credit validation handled by middleware.
219
- """
220
  user = req.state.user
221
  credits_reserved = req.state.credits_reserved
222
  job = await create_job(
@@ -247,10 +232,7 @@ async def analyze_image(
247
  request: AnalyzeImageRequest,
248
  db: AsyncSession = Depends(get_db)
249
  ):
250
- """
251
- Queue an image analysis job.
252
- Auth and credit validation handled by middleware.
253
- """
254
  user = req.state.user
255
  credits_reserved = req.state.credits_reserved
256
  job = await create_job(
@@ -283,28 +265,23 @@ async def get_jobs(
283
  page: int = 1,
284
  limit: int = 20
285
  ):
286
- """
287
- Get all jobs created by the current user.
288
- Returns a paginated list of jobs with status, type, and prompt (for video jobs).
289
- Auth handled by AuthMiddleware - user in request.state.user
290
- """
291
  user = req.state.user
292
  offset = (page - 1) * limit
293
 
294
- # Query jobs for the current user
295
  query = select(GeminiJob).where(
296
- GeminiJob.user_id == user.id # Integer FK comparison
297
  ).order_by(GeminiJob.created_at.desc()).offset(offset).limit(limit)
298
 
299
  result = await db.execute(query)
300
  jobs = result.scalars().all()
301
 
302
- # Get total count
303
- count_query = select(func.count()).where(GeminiJob.user_id == user.id) # Integer FK comparison
304
  count_result = await db.execute(count_query)
305
  total_count = count_result.scalar()
306
 
307
- # Build job list
308
  job_list = []
309
  for job in jobs:
310
  job_item = {
@@ -313,18 +290,18 @@ async def get_jobs(
313
  "status": job.status,
314
  "created_at": job.created_at.isoformat() if job.created_at else None,
315
  "completed_at": job.completed_at.isoformat() if job.completed_at else None,
316
- # "api_response": job.api_response,
317
  }
318
 
319
- # Include prompt for video jobs
320
  if job.job_type == "video" and job.input_data:
321
  job_item["prompt"] = job.input_data.get("prompt")
322
 
323
- # Include error message for failed jobs
324
  if job.status == "failed":
325
  job_item["error"] = job.error_message
326
 
327
- # Include download URL for completed video jobs
328
  if job.status == "completed" and job.job_type == "video" and job.output_data and job.output_data.get("filename"):
329
  job_item["download_url"] = f"/gemini/download/{job.job_id}"
330
 
@@ -345,16 +322,11 @@ async def get_job_status(
345
  req: Request,
346
  db: AsyncSession = Depends(get_db)
347
  ):
348
- """
349
- Get the status of a job.
350
- Poll this endpoint until status is 'completed' or 'failed'.
351
- For processing video jobs, this will check the Gemini API status and update the job.
352
- Auth handled by AuthMiddleware - user in request.state.user
353
- """
354
  user = req.state.user
355
  query = select(GeminiJob).where(
356
  GeminiJob.job_id == job_id,
357
- GeminiJob.user_id == user.id # Integer FK comparison
358
  )
359
  result = await db.execute(query)
360
  job = result.scalar_one_or_none()
@@ -365,7 +337,7 @@ async def get_job_status(
365
  detail="Job not found"
366
  )
367
 
368
- # If job is processing and is a video job, check Gemini API status and update
369
  if job.status == "processing" and job.job_type == "video" and job.third_party_id:
370
  from services.gemini_job_worker import GeminiJobProcessor
371
  processor = GeminiJobProcessor()
@@ -382,7 +354,7 @@ async def get_job_status(
382
  "credits_remaining": user.credits
383
  }
384
 
385
- # Include prompt for video jobs
386
  if job.job_type == "video" and job.input_data:
387
  response["prompt"] = job.input_data.get("prompt")
388
 
@@ -395,7 +367,7 @@ async def get_job_status(
395
  if job.status == "completed":
396
  response["completed_at"] = job.completed_at.isoformat() if job.completed_at else None
397
 
398
- # Return generated prompt if available (for animation_prompt jobs)
399
  if job.output_data and "prompt" in job.output_data:
400
  response["prompt"] = job.output_data["prompt"]
401
 
@@ -412,19 +384,14 @@ async def download_video(
412
  req: Request,
413
  db: AsyncSession = Depends(get_db)
414
  ):
415
- """
416
- Download a generated video.
417
- Downloads from Gemini URL, streams to client, then deletes local file.
418
- No permanent storage on server.
419
- Auth handled by AuthMiddleware - user in request.state.user
420
- """
421
  user = req.state.user
422
  from fastapi.responses import StreamingResponse
423
  import httpx
424
 
425
  query = select(GeminiJob).where(
426
  GeminiJob.job_id == job_id,
427
- GeminiJob.user_id == user.id, # Integer FK comparison
428
  GeminiJob.job_type == "video"
429
  )
430
  result = await db.execute(query)
@@ -449,8 +416,7 @@ async def download_video(
449
  detail="No video URL available"
450
  )
451
 
452
- # Stream video directly from Gemini URL to client
453
- # No local file storage needed
454
  async def stream_video():
455
  try:
456
  async with httpx.AsyncClient(timeout=120.0, follow_redirects=True) as client:
@@ -460,7 +426,7 @@ async def download_video(
460
  yield chunk
461
  except httpx.HTTPStatusError as e:
462
  if e.response.status_code in (401, 403, 404, 410):
463
- # Mark job as expired
464
  job.status = "expired"
465
  await db.commit()
466
 
@@ -493,16 +459,11 @@ async def cancel_job(
493
  req: Request,
494
  db: AsyncSession = Depends(get_db)
495
  ):
496
- """
497
- Cancel a queued job.
498
- Only jobs with status 'queued' can be cancelled.
499
- Processing/completed/failed jobs cannot be cancelled.
500
- Auth handled by AuthMiddleware - user in request.state.user
501
- """
502
  user = req.state.user
503
  query = select(GeminiJob).where(
504
  GeminiJob.job_id == job_id,
505
- GeminiJob.user_id == user.id # Integer FK comparison
506
  )
507
  result = await db.execute(query)
508
  job = result.scalar_one_or_none()
@@ -537,20 +498,12 @@ async def delete_job(
537
  req: Request,
538
  db: AsyncSession = Depends(get_db)
539
  ):
540
- """
541
- Soft delete a job with conditional credit refund.
542
-
543
- Refund policy:
544
- - If queued: Refund 8 credits (10 cost - 2 penalty), soft delete job.
545
- - If processing/completed/failed: Soft delete job (no refund).
546
- Auth handled by AuthMiddleware - user in request.state.user
547
- """
548
  user = req.state.user
549
  from services.db_service import QueryService
550
-
551
  qs = QueryService(user, db)
552
 
553
- # Get job (automatically filtered to user's job)
554
  job = await qs.select().execute_one(
555
  select(GeminiJob).where(GeminiJob.job_id == job_id)
556
  )
@@ -561,36 +514,32 @@ async def delete_job(
561
  detail="Job not found"
562
  )
563
 
564
- # Implement credit refund via CreditTransactionManager
565
  refund_amount = 0
566
  message = "Job deleted"
567
 
568
- # Determine refund based on job status
569
  if job.credits_reserved > 0 and not job.credits_refunded:
570
  from services.credit_service import CreditTransactionManager
571
 
572
  if not job.third_party_id:
573
- # Job never started (pre-execution failure)
574
  refund_amount = job.credits_reserved
575
  refund_reason = "Job deleted before execution"
576
 
577
  elif job.status == "queued":
578
- # Job queued - partial refund (2 credits cancellation fee)
579
  penalty = 2
580
  refund_amount = max(0, job.credits_reserved - penalty)
581
  refund_reason = f"Job cancelled while queued (penalty: {penalty} credits)"
582
 
583
  elif job.status in ["processing", "completed"]:
584
- # No refund for started/completed jobs
585
  refund_amount = 0
586
  refund_reason = None
587
 
588
  elif job.status == "failed":
589
- # Failed job - full refund
590
  refund_amount = job.credits_reserved
591
  refund_reason = "Job failed - full refund"
592
 
593
- # Process refund if applicable
594
  if refund_amount > 0:
595
  try:
596
  await CreditTransactionManager.add_credits(
@@ -616,7 +565,7 @@ async def delete_job(
616
  logger.error(f"Failed to refund credits for job {job_id}: {e}")
617
  message = "Job deleted (refund failed - contact support)"
618
 
619
- # Soft delete the job
620
  deleted = await qs.delete().soft_delete_one(job)
621
 
622
  if not deleted:
@@ -635,7 +584,5 @@ async def delete_job(
635
 
636
  @router.get("/models")
637
  async def get_models():
638
- """
639
- Get available model names.
640
- """
641
  return {"models": MODELS}
 
1
  """
2
  Gemini Router - API endpoints for Gemini AI services.
 
 
3
  """
4
  import os
5
  import uuid
 
18
  router = APIRouter(prefix="/gemini", tags=["gemini"])
19
 
20
 
21
+
22
  class GenerateAnimationPromptRequest(BaseModel):
23
  base64_image: str = Field(..., description="Base64 encoded image data")
24
  mime_type: str = Field(..., description="MIME type of the image (e.g., image/png)")
 
51
  prompt: str = Field(..., description="Analysis prompt")
52
 
53
 
54
+
 
55
 
56
 
57
  async def get_queue_position(db: AsyncSession, job_id: str) -> int:
 
71
  input_data: dict,
72
  credits_reserved: int = 0
73
  ) -> GeminiJob:
74
+ """Create a new job in the queue."""
75
  from services.gemini_job_worker import get_priority_for_job_type, get_pool
76
 
77
  job_id = f"job_{uuid.uuid4().hex[:16]}"
 
79
 
80
  job = GeminiJob(
81
  job_id=job_id,
82
+ user_id=user.id,
83
  job_type=job_type,
84
  status="queued",
85
  priority=priority,
86
  input_data=input_data,
87
+ credits_reserved=credits_reserved
88
  )
89
  db.add(job)
90
  await db.commit()
91
  await db.refresh(job)
92
 
93
+
94
  get_pool().notify_new_job(priority)
95
 
96
  return job
 
102
  request: GenerateAnimationPromptRequest,
103
  db: AsyncSession = Depends(get_db)
104
  ):
105
+ """Queue an animation prompt generation job."""
 
 
 
106
  user = req.state.user
107
  credits_reserved = req.state.credits_reserved
108
  job = await create_job(
 
134
  request: EditImageRequest,
135
  db: AsyncSession = Depends(get_db)
136
  ):
137
+ """Queue an image edit job."""
 
 
 
138
  user = req.state.user
139
  credits_reserved = req.state.credits_reserved
140
  job = await create_job(
 
166
  request: GenerateVideoRequest,
167
  db: AsyncSession = Depends(get_db)
168
  ):
169
+ """Queue a video generation job."""
 
 
 
170
  user = req.state.user
171
  credits_reserved = req.state.credits_reserved
172
  job = await create_job(
 
181
  "resolution": request.resolution,
182
  "number_of_videos": request.number_of_videos
183
  },
184
+ credits_reserved=credits_reserved
185
  )
186
 
187
  position = await get_queue_position(db, job.job_id)
 
201
  request: GenerateTextRequest,
202
  db: AsyncSession = Depends(get_db)
203
  ):
204
+ """Queue a text generation job."""
 
 
 
205
  user = req.state.user
206
  credits_reserved = req.state.credits_reserved
207
  job = await create_job(
 
232
  request: AnalyzeImageRequest,
233
  db: AsyncSession = Depends(get_db)
234
  ):
235
+ """Queue an image analysis job."""
 
 
 
236
  user = req.state.user
237
  credits_reserved = req.state.credits_reserved
238
  job = await create_job(
 
265
  page: int = 1,
266
  limit: int = 20
267
  ):
268
+ """Get all jobs for the current user."""
 
 
 
 
269
  user = req.state.user
270
  offset = (page - 1) * limit
271
 
272
+
273
  query = select(GeminiJob).where(
274
+ GeminiJob.user_id == user.id
275
  ).order_by(GeminiJob.created_at.desc()).offset(offset).limit(limit)
276
 
277
  result = await db.execute(query)
278
  jobs = result.scalars().all()
279
 
280
+ count_query = select(func.count()).where(GeminiJob.user_id == user.id)
 
281
  count_result = await db.execute(count_query)
282
  total_count = count_result.scalar()
283
 
284
+
285
  job_list = []
286
  for job in jobs:
287
  job_item = {
 
290
  "status": job.status,
291
  "created_at": job.created_at.isoformat() if job.created_at else None,
292
  "completed_at": job.completed_at.isoformat() if job.completed_at else None,
293
+
294
  }
295
 
296
+
297
  if job.job_type == "video" and job.input_data:
298
  job_item["prompt"] = job.input_data.get("prompt")
299
 
300
+
301
  if job.status == "failed":
302
  job_item["error"] = job.error_message
303
 
304
+
305
  if job.status == "completed" and job.job_type == "video" and job.output_data and job.output_data.get("filename"):
306
  job_item["download_url"] = f"/gemini/download/{job.job_id}"
307
 
 
322
  req: Request,
323
  db: AsyncSession = Depends(get_db)
324
  ):
325
+ """Get job status and update if processing."""
 
 
 
 
 
326
  user = req.state.user
327
  query = select(GeminiJob).where(
328
  GeminiJob.job_id == job_id,
329
+ GeminiJob.user_id == user.id
330
  )
331
  result = await db.execute(query)
332
  job = result.scalar_one_or_none()
 
337
  detail="Job not found"
338
  )
339
 
340
+
341
  if job.status == "processing" and job.job_type == "video" and job.third_party_id:
342
  from services.gemini_job_worker import GeminiJobProcessor
343
  processor = GeminiJobProcessor()
 
354
  "credits_remaining": user.credits
355
  }
356
 
357
+
358
  if job.job_type == "video" and job.input_data:
359
  response["prompt"] = job.input_data.get("prompt")
360
 
 
367
  if job.status == "completed":
368
  response["completed_at"] = job.completed_at.isoformat() if job.completed_at else None
369
 
370
+
371
  if job.output_data and "prompt" in job.output_data:
372
  response["prompt"] = job.output_data["prompt"]
373
 
 
384
  req: Request,
385
  db: AsyncSession = Depends(get_db)
386
  ):
387
+ """Stream video from Gemini to client."""
 
 
 
 
 
388
  user = req.state.user
389
  from fastapi.responses import StreamingResponse
390
  import httpx
391
 
392
  query = select(GeminiJob).where(
393
  GeminiJob.job_id == job_id,
394
+ GeminiJob.user_id == user.id,
395
  GeminiJob.job_type == "video"
396
  )
397
  result = await db.execute(query)
 
416
  detail="No video URL available"
417
  )
418
 
419
+
 
420
  async def stream_video():
421
  try:
422
  async with httpx.AsyncClient(timeout=120.0, follow_redirects=True) as client:
 
426
  yield chunk
427
  except httpx.HTTPStatusError as e:
428
  if e.response.status_code in (401, 403, 404, 410):
429
+
430
  job.status = "expired"
431
  await db.commit()
432
 
 
459
  req: Request,
460
  db: AsyncSession = Depends(get_db)
461
  ):
462
+ """Cancel a queued job."""
 
 
 
 
 
463
  user = req.state.user
464
  query = select(GeminiJob).where(
465
  GeminiJob.job_id == job_id,
466
+ GeminiJob.user_id == user.id
467
  )
468
  result = await db.execute(query)
469
  job = result.scalar_one_or_none()
 
498
  req: Request,
499
  db: AsyncSession = Depends(get_db)
500
  ):
501
+ """Delete job with conditional credit refund."""
 
 
 
 
 
 
 
502
  user = req.state.user
503
  from services.db_service import QueryService
 
504
  qs = QueryService(user, db)
505
 
506
+
507
  job = await qs.select().execute_one(
508
  select(GeminiJob).where(GeminiJob.job_id == job_id)
509
  )
 
514
  detail="Job not found"
515
  )
516
 
517
+
518
  refund_amount = 0
519
  message = "Job deleted"
520
 
521
+
522
  if job.credits_reserved > 0 and not job.credits_refunded:
523
  from services.credit_service import CreditTransactionManager
524
 
525
  if not job.third_party_id:
 
526
  refund_amount = job.credits_reserved
527
  refund_reason = "Job deleted before execution"
528
 
529
  elif job.status == "queued":
 
530
  penalty = 2
531
  refund_amount = max(0, job.credits_reserved - penalty)
532
  refund_reason = f"Job cancelled while queued (penalty: {penalty} credits)"
533
 
534
  elif job.status in ["processing", "completed"]:
 
535
  refund_amount = 0
536
  refund_reason = None
537
 
538
  elif job.status == "failed":
 
539
  refund_amount = job.credits_reserved
540
  refund_reason = "Job failed - full refund"
541
 
542
+
543
  if refund_amount > 0:
544
  try:
545
  await CreditTransactionManager.add_credits(
 
565
  logger.error(f"Failed to refund credits for job {job_id}: {e}")
566
  message = "Job deleted (refund failed - contact support)"
567
 
568
+
569
  deleted = await qs.delete().soft_delete_one(job)
570
 
571
  if not deleted:
 
584
 
585
  @router.get("/models")
586
  async def get_models():
587
+ """Get available model names."""
 
 
588
  return {"models": MODELS}
run_credit_tests.py DELETED
@@ -1,61 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Run Credit Service Test Suite
4
-
5
- Runs all credit service tests and generates a coverage report.
6
- """
7
- import sys
8
- import subprocess
9
-
10
- def run_tests():
11
- """Run pytest for credit service tests."""
12
-
13
- print("=" * 70)
14
- print("Running Credit Service Test Suite")
15
- print("=" * 70)
16
- print()
17
-
18
- test_files = [
19
- "tests/test_credit_transaction_manager.py",
20
- "tests/test_response_inspector.py",
21
- "tests/test_credit_middleware_integration.py"
22
- ]
23
-
24
- cmd = [
25
- "pytest",
26
- *test_files,
27
- "-v", # Verbose
28
- "--tb=short", # Short traceback
29
- "--cov=services/credit_service", # Coverage for credit service
30
- "--cov-report=term-missing", # Show missing lines
31
- "--cov-report=html:htmlcov/credit_service", # HTML report
32
- ]
33
-
34
- print(f"Command: {' '.join(cmd)}")
35
- print()
36
-
37
- try:
38
- result = subprocess.run(cmd, check=False)
39
-
40
- print()
41
- print("=" * 70)
42
- if result.returncode == 0:
43
- print("βœ… All tests passed!")
44
- else:
45
- print("❌ Some tests failed!")
46
- print("=" * 70)
47
- print()
48
- print("Coverage report generated at: htmlcov/credit_service/index.html")
49
-
50
- return result.returncode
51
-
52
- except FileNotFoundError:
53
- print("❌ pytest not found. Install it with: pip install pytest pytest-asyncio pytest-cov")
54
- return 1
55
- except Exception as e:
56
- print(f"❌ Error running tests: {e}")
57
- return 1
58
-
59
-
60
- if __name__ == "__main__":
61
- sys.exit(run_tests())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
run_tests.sh DELETED
@@ -1,13 +0,0 @@
1
- #!/bin/bash
2
- # Test runner for DB Service
3
-
4
- export ADMIN_EMAILS="admin@example.com"
5
- export PYTHONPATH="$(pwd):$PYTHONPATH"
6
-
7
- echo "Running DB Service Tests..."
8
- echo "=============================="
9
-
10
- python -m pytest tests/test_db_service.py -v --tb=short
11
-
12
- echo ""
13
- echo "Test run complete!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
services/credit_service/middleware.py CHANGED
@@ -1,10 +1,6 @@
1
  """
2
- Credit Middleware - Automatic request/response-based credit management.
3
-
4
- Intercepts requests to reserve credits and inspects responses to automatically
5
- confirm or refund based on operation success/failure.
6
-
7
- Application layer is completely unaware of credit management.
8
  """
9
 
10
  import logging
@@ -31,18 +27,9 @@ logger = logging.getLogger(__name__)
31
 
32
  class CreditMiddleware(BaseServiceMiddleware):
33
  """
34
- Credit middleware with automatic request/response-based credit management.
35
-
36
- Application layer is completely unaware of credits.
37
- Credits are reserved on request, confirmed/refunded based on response.
38
-
39
- Flow:
40
- 1. REQUEST PHASE: Reserve credits if endpoint requires them
41
- 2. APPLICATION: Process request (unaware of credits)
42
- 3. RESPONSE PHASE: Inspect response and confirm/refund credits
43
-
44
- NOTE: This middleware MUST run AFTER AuthMiddleware since it needs
45
- the authenticated user from request.state.user
46
  """
47
 
48
  SERVICE_NAME = "credit"
@@ -50,22 +37,21 @@ class CreditMiddleware(BaseServiceMiddleware):
50
  async def dispatch(self, request: Request, call_next):
51
  """Process request through credit middleware."""
52
 
53
- # Skip OPTIONS requests (CORS preflight)
54
  if request.method == "OPTIONS":
55
  return await call_next(request)
56
 
57
  path = request.url.path
58
 
59
- # Get endpoint configuration
60
  config = CreditServiceConfig.get_config(path)
61
  credit_cost = config.get("cost", 0)
62
  endpoint_type = config.get("type", "free")
63
 
64
- if credit_cost == 0:
65
- # Free endpoint, no credit handling needed
66
  return await call_next(request)
67
 
68
- # User must be authenticated (set by AuthMiddleware)
69
  user = getattr(request.state, 'user', None)
70
  if not user:
71
  return JSONResponse(
@@ -73,12 +59,9 @@ class CreditMiddleware(BaseServiceMiddleware):
73
  content={"detail": "Authentication required for this endpoint"}
74
  )
75
 
76
- # ===================================================================
77
- # REQUEST PHASE: Reserve Credits
78
- # ===================================================================
79
  async with async_session_maker() as db:
80
- try:
81
- # Reserve credits
82
  transaction = await CreditTransactionManager.reserve_credits(
83
  session=db,
84
  user=user,
@@ -96,7 +79,7 @@ class CreditMiddleware(BaseServiceMiddleware):
96
 
97
  await db.commit()
98
 
99
- # Attach transaction info to request for response phase
100
  request.state.credit_transaction_id = transaction.transaction_id
101
  request.state.endpoint_type = endpoint_type
102
  request.state.credit_cost = credit_cost
@@ -124,30 +107,24 @@ class CreditMiddleware(BaseServiceMiddleware):
124
  content={"detail": "Failed to reserve credits"}
125
  )
126
 
127
- # ===================================================================
128
- # CALL APPLICATION LAYER (completely unaware of credits)
129
- # ===================================================================
130
  response = await call_next(request)
131
 
132
- # ===================================================================
133
- # RESPONSE PHASE: Confirm or Refund Based on Response
134
- # ===================================================================
135
  transaction_id = getattr(request.state, 'credit_transaction_id', None)
136
- if not transaction_id:
137
- # No transaction to finalize (shouldn't happen, but be safe)
138
  return response
139
 
140
- # We need to read the response body to inspect it
141
- # This requires special handling to not consume the response
142
  response_body = b""
143
  async for chunk in response.body_iterator:
144
  response_body += chunk
145
 
146
- # Parse response for inspection
147
  response_data = ResponseInspector.parse_response_body(response_body)
148
  inspector = ResponseInspector()
149
 
150
- # Determine credit action based on response
151
  async with async_session_maker() as db:
152
  try:
153
  should_confirm = inspector.should_confirm(
@@ -157,8 +134,7 @@ class CreditMiddleware(BaseServiceMiddleware):
157
  response, endpoint_type, response_data
158
  )
159
 
160
- if should_confirm:
161
- # Operation successful - confirm credits
162
  await CreditTransactionManager.confirm_credits(
163
  session=db,
164
  transaction_id=transaction_id,
@@ -174,8 +150,7 @@ class CreditMiddleware(BaseServiceMiddleware):
174
  f"Credits confirmed for {transaction_id} (success)"
175
  )
176
 
177
- elif should_refund:
178
- # Operation failed - refund credits
179
  reason = inspector.get_refund_reason(response, response_data)
180
  await CreditTransactionManager.refund_credits(
181
  session=db,
@@ -194,20 +169,16 @@ class CreditMiddleware(BaseServiceMiddleware):
194
  f"Credits refunded for {transaction_id}: {reason}"
195
  )
196
 
197
- else:
198
- # Keep reserved (async operation pending completion)
199
  self.log_request(
200
  request,
201
  f"Credits kept reserved for {transaction_id} (async pending)"
202
  )
203
 
204
  except Exception as e:
205
- logger.error(f"Response phase credit handling failed: {e}", exc_info=True)
206
- # Don't fail the actual response, just log the error
207
- # The transaction remains reserved and can be manually resolved
208
 
209
- # Reconstruct response with original body
210
- # This is necessary because we consumed the body_iterator above
211
  return Response(
212
  content=response_body,
213
  status_code=response.status_code,
 
1
  """
2
+ Credit Middleware - Automatic credit management.
3
+ Reserves credits on request, confirms/refunds based on response.
 
 
 
 
4
  """
5
 
6
  import logging
 
27
 
28
  class CreditMiddleware(BaseServiceMiddleware):
29
  """
30
+ Credit middleware with automatic credit management.
31
+ Reserves credits on request, confirms/refunds based on response.
32
+ Must run after AuthMiddleware.
 
 
 
 
 
 
 
 
 
33
  """
34
 
35
  SERVICE_NAME = "credit"
 
37
  async def dispatch(self, request: Request, call_next):
38
  """Process request through credit middleware."""
39
 
40
+
41
  if request.method == "OPTIONS":
42
  return await call_next(request)
43
 
44
  path = request.url.path
45
 
46
+
47
  config = CreditServiceConfig.get_config(path)
48
  credit_cost = config.get("cost", 0)
49
  endpoint_type = config.get("type", "free")
50
 
51
+
 
52
  return await call_next(request)
53
 
54
+
55
  user = getattr(request.state, 'user', None)
56
  if not user:
57
  return JSONResponse(
 
59
  content={"detail": "Authentication required for this endpoint"}
60
  )
61
 
62
+
 
 
63
  async with async_session_maker() as db:
64
+
 
65
  transaction = await CreditTransactionManager.reserve_credits(
66
  session=db,
67
  user=user,
 
79
 
80
  await db.commit()
81
 
82
+
83
  request.state.credit_transaction_id = transaction.transaction_id
84
  request.state.endpoint_type = endpoint_type
85
  request.state.credit_cost = credit_cost
 
107
  content={"detail": "Failed to reserve credits"}
108
  )
109
 
110
+
 
 
111
  response = await call_next(request)
112
 
113
+
 
 
114
  transaction_id = getattr(request.state, 'credit_transaction_id', None)
115
+
 
116
  return response
117
 
118
+
 
119
  response_body = b""
120
  async for chunk in response.body_iterator:
121
  response_body += chunk
122
 
123
+
124
  response_data = ResponseInspector.parse_response_body(response_body)
125
  inspector = ResponseInspector()
126
 
127
+
128
  async with async_session_maker() as db:
129
  try:
130
  should_confirm = inspector.should_confirm(
 
134
  response, endpoint_type, response_data
135
  )
136
 
137
+
 
138
  await CreditTransactionManager.confirm_credits(
139
  session=db,
140
  transaction_id=transaction_id,
 
150
  f"Credits confirmed for {transaction_id} (success)"
151
  )
152
 
153
+
 
154
  reason = inspector.get_refund_reason(response, response_data)
155
  await CreditTransactionManager.refund_credits(
156
  session=db,
 
169
  f"Credits refunded for {transaction_id}: {reason}"
170
  )
171
 
172
+
 
173
  self.log_request(
174
  request,
175
  f"Credits kept reserved for {transaction_id} (async pending)"
176
  )
177
 
178
  except Exception as e:
179
+
 
 
180
 
181
+
 
182
  return Response(
183
  content=response_body,
184
  status_code=response.status_code,
test_token_expiry.py DELETED
@@ -1,106 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Simple JWT Token Expiry Test - Direct approach
4
- """
5
- import os
6
- import jwt
7
- import time
8
- from datetime import datetime, timedelta
9
- from dotenv import load_dotenv
10
-
11
- # Load environment variables
12
- load_dotenv()
13
-
14
- def test_token_expiry():
15
- """Test JWT token creation and expiry"""
16
-
17
- print("=" * 70)
18
- print("JWT Token Expiry Test")
19
- print("=" * 70)
20
-
21
- # Get environment variables
22
- access_expiry_minutes = int(os.getenv("JWT_ACCESS_EXPIRY_MINUTES", "15"))
23
- jwt_secret = os.getenv("JWT_SECRET")
24
- jwt_algorithm = os.getenv("JWT_ALGORITHM", "HS256")
25
-
26
- print(f"\nπŸ“‹ Current Configuration:")
27
- print(f" JWT_ACCESS_EXPIRY_MINUTES: {access_expiry_minutes} minute(s)")
28
- print(f" JWT_ALGORITHM: {jwt_algorithm}")
29
- print(f" JWT_SECRET: {'βœ… Set' if jwt_secret else '❌ NOT SET'}")
30
-
31
- if not jwt_secret:
32
- print("\n❌ ERROR: JWT_SECRET not set!")
33
- return
34
-
35
- # Create token manually
36
- print(f"\nπŸ” Creating test token...")
37
- now = datetime.utcnow()
38
- expires_at = now + timedelta(minutes=access_expiry_minutes)
39
-
40
- payload = {
41
- "sub": "test_user_123",
42
- "email": "test@example.com",
43
- "type": "access",
44
- "tv": 1,
45
- "iat": now,
46
- "exp": expires_at
47
- }
48
-
49
- token = jwt.encode(payload, jwt_secret, algorithm=jwt_algorithm)
50
- print(f" βœ… Token created")
51
- print(f" Issued at: {now}")
52
- print(f" Expires at: {expires_at}")
53
- print(f" Token lifetime: {access_expiry_minutes} minute(s)")
54
-
55
- # Verify token is valid now
56
- print(f"\nβœ… Verifying token immediately...")
57
- try:
58
- decoded = jwt.decode(token, jwt_secret, algorithms=[jwt_algorithm])
59
- print(f" βœ… Token is valid")
60
- print(f" User: {decoded['sub']}")
61
- print(f" Email: {decoded['email']}")
62
- except jwt.ExpiredSignatureError:
63
- print(f" ❌ Token already expired (shouldn't happen!)")
64
- return
65
- except Exception as e:
66
- print(f" ❌ Error: {e}")
67
- return
68
-
69
- # If very short expiry, wait and test
70
- if access_expiry_minutes <= 5:
71
- wait_seconds = (access_expiry_minutes * 60) + 5
72
- print(f"\n⏳ Waiting {wait_seconds} seconds for token to expire...")
73
- print(f" Token should expire at: {expires_at}")
74
-
75
- for i in range(wait_seconds):
76
- remaining = wait_seconds - i
77
- if remaining % 10 == 0 or remaining <= 10:
78
- print(f" ⏱️ {remaining} seconds remaining...")
79
- time.sleep(1)
80
-
81
- print(f"\nπŸ” Testing token after {wait_seconds} seconds...")
82
- print(f" Current time: {datetime.utcnow()}")
83
-
84
- try:
85
- decoded = jwt.decode(token, jwt_secret, algorithms=[jwt_algorithm])
86
- print(f" ❌ Token STILL VALID (This is a problem!)")
87
- print(f" This means the token didn't expire as expected")
88
- except jwt.ExpiredSignatureError:
89
- print(f" βœ… Token EXPIRED (This is correct!)")
90
- print(f" Token expiry is working properly")
91
- except Exception as e:
92
- print(f" ⚠️ Error: {e}")
93
- else:
94
- print(f"\nπŸ’‘ Token expiry is set to {access_expiry_minutes} minutes")
95
- print(f" This is too long to wait in this test")
96
- print(f" To test expiry quickly:")
97
- print(f" 1. Set JWT_ACCESS_EXPIRY_MINUTES=1 in .env")
98
- print(f" 2. Restart the server")
99
- print(f" 3. Run this test again")
100
-
101
- print("\n" + "=" * 70)
102
- print("Test Complete!")
103
- print("=" * 70)
104
-
105
- if __name__ == "__main__":
106
- test_token_expiry()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/CREDIT_TESTS_README.md DELETED
@@ -1,177 +0,0 @@
1
- # Credit Service Test Suite
2
-
3
- ## Overview
4
-
5
- Comprehensive test suite for the middleware-centric credit service covering:
6
- - Transaction Manager operations
7
- - Response inspection logic
8
- - Middleware integration
9
- - End-to-end credit flows
10
-
11
- ## Test Files
12
-
13
- ### 1. `test_credit_transaction_manager.py`
14
- Tests core credit transaction operations:
15
- - βœ… Reserve credits (success & insufficient funds)
16
- - βœ… Confirm credits
17
- - βœ… Refund credits
18
- - βœ… Add credits (purchases)
19
- - βœ… Balance verification
20
- - βœ… Transaction history queries
21
- - βœ… Full transaction flows (reserveβ†’confirm, reserveβ†’refund)
22
-
23
- **Coverage:** All `CreditTransactionManager` methods and error scenarios
24
-
25
- ### 2. `test_response_inspector.py`
26
- Tests response analysis logic:
27
- - βœ… Sync endpoint success/failure detection
28
- - βœ… Async job creation handling
29
- - βœ… Async job status checks (queued, processing, completed, failed)
30
- - βœ… Refundable vs non-refundable error classification
31
- - βœ… Refund reason generation
32
- - βœ… JSON parsing utilities
33
-
34
- **Coverage:** All `ResponseInspector` methods and edge cases
35
-
36
- ### 3. `test_credit_middleware_integration.py`
37
- Tests middleware end-to-end flow:
38
- - βœ… Free endpoint bypass
39
- - βœ… Authentication enforcement
40
- - βœ… Credit reservation on request
41
- - βœ… Insufficient credits rejection
42
- - βœ… Automatic confirmation on success
43
- - βœ… Automatic refund on failure
44
- - βœ… Async operation handling
45
- - βœ… Error handling & resilience
46
-
47
- **Coverage:** Complete middleware request/response cycle
48
-
49
- ## Running Tests
50
-
51
- ### Quick Run
52
- ```bash
53
- python3 run_credit_tests.py
54
- ```
55
-
56
- ### Manual Run with Coverage
57
- ```bash
58
- pytest tests/test_credit*.py -v --cov=services/credit_service --cov-report=html
59
- ```
60
-
61
- ### Run Specific Test File
62
- ```bash
63
- pytest tests/test_credit_transaction_manager.py -v
64
- pytest tests/test_response_inspector.py -v
65
- pytest tests/test_credit_middleware_integration.py -v
66
- ```
67
-
68
- ### Run Specific Test
69
- ```bash
70
- pytest tests/test_credit_transaction_manager.py::test_reserve_credits_success -v
71
- ```
72
-
73
- ## Test Coverage Goals
74
-
75
- | Component | Target Coverage | Status |
76
- |-----------|----------------|--------|
77
- | CreditTransactionManager | 90%+ | βœ… |
78
- | ResponseInspector | 95%+ | βœ… |
79
- | CreditMiddleware | 85%+ | βœ… |
80
- | Overall Credit Service | 90%+ | 🎯 |
81
-
82
- ## Test Scenarios Covered
83
-
84
- ### Happy Paths
85
- - [x] Successful credit reservation
86
- - [x] Successful credit confirmation
87
- - [x] Successful credit refund
88
- - [x] Successful credit purchase
89
- - [x] Sync operation success β†’ confirm
90
- - [x] Async job completion β†’ confirm
91
-
92
- ### Error Paths
93
- - [x] Insufficient credits
94
- - [x] Transaction not found
95
- - [x] User not found
96
- - [x] Sync operation failure β†’ refund
97
- - [x] Async job failure (refundable) β†’ refund
98
- - [x] Async job failure (non-refundable) β†’ keep deducted
99
- - [x] Database errors during operations
100
- - [x] Malformed responses
101
-
102
- ### Edge Cases
103
- - [x] Exact balance reservation
104
- - [x] Free endpoint (cost=0)
105
- - [x] Unauthenticated requests
106
- - [x] OPTIONS requests
107
- - [x] Missing status field in async response
108
- - [x] Response phase errors don't break actual response
109
-
110
- ## Dependencies
111
-
112
- Install test dependencies:
113
- ```bash
114
- pip install pytest pytest-asyncio pytest-cov aiosqlite
115
- ```
116
-
117
- ## Test Database
118
-
119
- Tests use in-memory SQLite database (`sqlite+aiosqlite:///:memory:`) for:
120
- - Fast execution
121
- - No side effects
122
- - Complete isolation
123
- - Automatic cleanup
124
-
125
- ## Continuous Integration
126
-
127
- Add to CI pipeline:
128
- ```yaml
129
- # .github/workflows/test.yml
130
- - name: Run Credit Service Tests
131
- run: python3 run_credit_tests.py
132
-
133
- - name: Upload Coverage
134
- uses: codecov/codecov-action@v3
135
- with:
136
- files: ./htmlcov/credit_service/coverage.xml
137
- ```
138
-
139
- ## Future Test Additions
140
-
141
- - [ ] Concurrent operation tests (race conditions)
142
- - [ ] Load testing (many simultaneous requests)
143
- - [ ] API endpoint tests (/credits/transactions, /credits/balance/verify)
144
- - [ ] Payment integration tests
145
- - [ ] Job lifecycle integration tests
146
- - [ ] Performance benchmarks
147
-
148
- ## Test Conventions
149
-
150
- - **Fixtures**: Defined at file level, reusable across tests
151
- - **Naming**: `test_<component>_<scenario>` format
152
- - **Assertions**: Multiple assertions per test acceptable for related checks
153
- - **Async**: All database tests use `@pytest.mark.asyncio`
154
- - **Mocking**: Use `unittest.mock` for external dependencies
155
- - **Cleanup**: Automatic via fixtures, no manual teardown needed
156
-
157
- ## Troubleshooting
158
-
159
- **Issue:** `ModuleNotFoundError: No module named 'pytest'`
160
- **Fix:** `pip install pytest pytest-asyncio`
161
-
162
- **Issue:** Tests fail with database errors
163
- **Fix:** Ensure `aiosqlite` is installed: `pip install aiosqlite`
164
-
165
- **Issue:** Coverage report not generated
166
- **Fix:** Install coverage tools: `pip install pytest-cov`
167
-
168
- **Issue:** Import errors for credit service modules
169
- **Fix:** Run tests from project root directory
170
-
171
- ## Maintenance
172
-
173
- When adding new credit service features:
174
- 1. Add tests to appropriate test file
175
- 2. Run full test suite to ensure no regressions
176
- 3. Update coverage target if needed
177
- 4. Document new test scenarios in this README