Spaces:
Sleeping
Sleeping
payment id
Browse files- app.py +2 -1
- core/models.py +19 -0
- routers/contact.py +98 -0
- routers/payments.py +27 -6
app.py
CHANGED
|
@@ -12,7 +12,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
| 12 |
from fastapi.responses import JSONResponse
|
| 13 |
|
| 14 |
from core.database import init_db
|
| 15 |
-
from routers import auth, blink, credits, general, gemini, payments
|
| 16 |
from services.drive_service import DriveService
|
| 17 |
|
| 18 |
# Configure logging
|
|
@@ -88,6 +88,7 @@ app.include_router(blink.router)
|
|
| 88 |
app.include_router(gemini.router)
|
| 89 |
app.include_router(credits.router)
|
| 90 |
app.include_router(payments.router)
|
|
|
|
| 91 |
|
| 92 |
|
| 93 |
@app.exception_handler(Exception)
|
|
|
|
| 12 |
from fastapi.responses import JSONResponse
|
| 13 |
|
| 14 |
from core.database import init_db
|
| 15 |
+
from routers import auth, blink, contact, credits, general, gemini, payments
|
| 16 |
from services.drive_service import DriveService
|
| 17 |
|
| 18 |
# Configure logging
|
|
|
|
| 88 |
app.include_router(gemini.router)
|
| 89 |
app.include_router(credits.router)
|
| 90 |
app.include_router(payments.router)
|
| 91 |
+
app.include_router(contact.router)
|
| 92 |
|
| 93 |
|
| 94 |
@app.exception_handler(Exception)
|
core/models.py
CHANGED
|
@@ -190,3 +190,22 @@ class PaymentTransaction(Base):
|
|
| 190 |
|
| 191 |
def __repr__(self):
|
| 192 |
return f"<PaymentTransaction(id={self.transaction_id}, status={self.status}, amount={self.amount_paise})>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
def __repr__(self):
|
| 192 |
return f"<PaymentTransaction(id={self.transaction_id}, status={self.status}, amount={self.amount_paise})>"
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
class Contact(Base):
|
| 196 |
+
"""
|
| 197 |
+
Contact form submissions from authenticated users.
|
| 198 |
+
For customer support inquiries.
|
| 199 |
+
"""
|
| 200 |
+
__tablename__ = "contacts"
|
| 201 |
+
|
| 202 |
+
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
| 203 |
+
user_id = Column(String(50), index=True, nullable=False) # Links to User.user_id
|
| 204 |
+
email = Column(String(255), nullable=False, index=True) # User's email
|
| 205 |
+
subject = Column(String(500), nullable=True)
|
| 206 |
+
message = Column(Text, nullable=False)
|
| 207 |
+
ip_address = Column(String(45), nullable=True) # IPv6 can be up to 45 chars
|
| 208 |
+
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
| 209 |
+
|
| 210 |
+
def __repr__(self):
|
| 211 |
+
return f"<Contact(id={self.id}, user_id={self.user_id}, email={self.email})>"
|
routers/contact.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Contact Router - API endpoint for customer support contact form.
|
| 3 |
+
|
| 4 |
+
Endpoints:
|
| 5 |
+
- POST /contact - Submit a contact form (requires authentication)
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import logging
|
| 9 |
+
from typing import Optional
|
| 10 |
+
|
| 11 |
+
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
| 12 |
+
from pydantic import BaseModel, EmailStr
|
| 13 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 14 |
+
|
| 15 |
+
from core.database import get_db
|
| 16 |
+
from core.models import User, Contact
|
| 17 |
+
from dependencies import get_current_user
|
| 18 |
+
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
router = APIRouter(prefix="/contact", tags=["contact"])
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# =============================================================================
|
| 25 |
+
# Request/Response Models
|
| 26 |
+
# =============================================================================
|
| 27 |
+
|
| 28 |
+
class ContactRequest(BaseModel):
|
| 29 |
+
"""Request to submit a contact form."""
|
| 30 |
+
subject: Optional[str] = None
|
| 31 |
+
message: str
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class ContactResponse(BaseModel):
|
| 35 |
+
"""Response after contact form submission."""
|
| 36 |
+
success: bool
|
| 37 |
+
message: str
|
| 38 |
+
email: str # Return user's email for confirmation
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# =============================================================================
|
| 42 |
+
# Endpoints
|
| 43 |
+
# =============================================================================
|
| 44 |
+
|
| 45 |
+
@router.post("", response_model=ContactResponse)
|
| 46 |
+
async def submit_contact(
|
| 47 |
+
request_body: ContactRequest,
|
| 48 |
+
request: Request,
|
| 49 |
+
user: User = Depends(get_current_user),
|
| 50 |
+
db: AsyncSession = Depends(get_db)
|
| 51 |
+
):
|
| 52 |
+
"""
|
| 53 |
+
Submit a contact form for customer support.
|
| 54 |
+
|
| 55 |
+
Requires authentication - user must be logged in.
|
| 56 |
+
"""
|
| 57 |
+
# Validate message
|
| 58 |
+
if not request_body.message or not request_body.message.strip():
|
| 59 |
+
raise HTTPException(
|
| 60 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 61 |
+
detail="Message cannot be empty"
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
# Get client IP
|
| 65 |
+
client_ip = request.headers.get("X-Forwarded-For", request.client.host if request.client else None)
|
| 66 |
+
if client_ip:
|
| 67 |
+
client_ip = client_ip.split(",")[0].strip()
|
| 68 |
+
|
| 69 |
+
try:
|
| 70 |
+
# Create contact record
|
| 71 |
+
contact = Contact(
|
| 72 |
+
user_id=user.user_id,
|
| 73 |
+
email=user.email,
|
| 74 |
+
subject=request_body.subject.strip() if request_body.subject else None,
|
| 75 |
+
message=request_body.message.strip(),
|
| 76 |
+
ip_address=client_ip
|
| 77 |
+
)
|
| 78 |
+
db.add(contact)
|
| 79 |
+
await db.commit()
|
| 80 |
+
|
| 81 |
+
logger.info(
|
| 82 |
+
f"Contact form submitted: user={user.user_id}, email={user.email}, "
|
| 83 |
+
f"subject={request_body.subject}"
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
return ContactResponse(
|
| 87 |
+
success=True,
|
| 88 |
+
message="Your message has been received. We will get back to you shortly.",
|
| 89 |
+
email=user.email
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
except Exception as e:
|
| 93 |
+
logger.error(f"Error saving contact form: {e}")
|
| 94 |
+
await db.rollback()
|
| 95 |
+
raise HTTPException(
|
| 96 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 97 |
+
detail="Failed to submit contact form. Please try again."
|
| 98 |
+
)
|
routers/payments.py
CHANGED
|
@@ -14,9 +14,9 @@ import uuid
|
|
| 14 |
from datetime import datetime
|
| 15 |
from typing import List, Optional
|
| 16 |
|
| 17 |
-
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
| 18 |
from pydantic import BaseModel
|
| 19 |
-
from sqlalchemy import select, desc
|
| 20 |
from sqlalchemy.ext.asyncio import AsyncSession
|
| 21 |
|
| 22 |
from core.database import get_db
|
|
@@ -93,6 +93,7 @@ class VerifyPaymentResponse(BaseModel):
|
|
| 93 |
class PaymentHistoryItem(BaseModel):
|
| 94 |
"""Single payment in history."""
|
| 95 |
transaction_id: str
|
|
|
|
| 96 |
package_id: str
|
| 97 |
credits_amount: int
|
| 98 |
amount_paise: int
|
|
@@ -105,9 +106,11 @@ class PaymentHistoryItem(BaseModel):
|
|
| 105 |
|
| 106 |
|
| 107 |
class PaymentHistoryResponse(BaseModel):
|
| 108 |
-
"""Payment history response."""
|
| 109 |
transactions: List[PaymentHistoryItem]
|
| 110 |
total_count: int
|
|
|
|
|
|
|
| 111 |
|
| 112 |
|
| 113 |
# =============================================================================
|
|
@@ -454,18 +457,33 @@ async def razorpay_webhook(
|
|
| 454 |
|
| 455 |
@router.get("/history", response_model=PaymentHistoryResponse)
|
| 456 |
async def get_payment_history(
|
|
|
|
|
|
|
| 457 |
user: User = Depends(get_current_user),
|
| 458 |
db: AsyncSession = Depends(get_db)
|
| 459 |
):
|
| 460 |
"""
|
| 461 |
-
Get user's payment history.
|
| 462 |
|
| 463 |
-
Returns
|
| 464 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 465 |
result = await db.execute(
|
| 466 |
select(PaymentTransaction)
|
| 467 |
.where(PaymentTransaction.user_id == user.user_id)
|
| 468 |
.order_by(desc(PaymentTransaction.created_at))
|
|
|
|
|
|
|
| 469 |
)
|
| 470 |
transactions = result.scalars().all()
|
| 471 |
|
|
@@ -473,6 +491,7 @@ async def get_payment_history(
|
|
| 473 |
for txn in transactions:
|
| 474 |
history.append(PaymentHistoryItem(
|
| 475 |
transaction_id=txn.transaction_id,
|
|
|
|
| 476 |
package_id=txn.package_id,
|
| 477 |
credits_amount=txn.credits_amount,
|
| 478 |
amount_paise=txn.amount_paise,
|
|
@@ -486,5 +505,7 @@ async def get_payment_history(
|
|
| 486 |
|
| 487 |
return PaymentHistoryResponse(
|
| 488 |
transactions=history,
|
| 489 |
-
total_count=
|
|
|
|
|
|
|
| 490 |
)
|
|
|
|
| 14 |
from datetime import datetime
|
| 15 |
from typing import List, Optional
|
| 16 |
|
| 17 |
+
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
| 18 |
from pydantic import BaseModel
|
| 19 |
+
from sqlalchemy import select, desc, func
|
| 20 |
from sqlalchemy.ext.asyncio import AsyncSession
|
| 21 |
|
| 22 |
from core.database import get_db
|
|
|
|
| 93 |
class PaymentHistoryItem(BaseModel):
|
| 94 |
"""Single payment in history."""
|
| 95 |
transaction_id: str
|
| 96 |
+
razorpay_payment_id: Optional[str] = None # Razorpay payment ID
|
| 97 |
package_id: str
|
| 98 |
credits_amount: int
|
| 99 |
amount_paise: int
|
|
|
|
| 106 |
|
| 107 |
|
| 108 |
class PaymentHistoryResponse(BaseModel):
|
| 109 |
+
"""Payment history response with pagination."""
|
| 110 |
transactions: List[PaymentHistoryItem]
|
| 111 |
total_count: int
|
| 112 |
+
page: int
|
| 113 |
+
limit: int
|
| 114 |
|
| 115 |
|
| 116 |
# =============================================================================
|
|
|
|
| 457 |
|
| 458 |
@router.get("/history", response_model=PaymentHistoryResponse)
|
| 459 |
async def get_payment_history(
|
| 460 |
+
page: int = Query(1, ge=1, description="Page number"),
|
| 461 |
+
limit: int = Query(20, ge=1, le=100, description="Items per page"),
|
| 462 |
user: User = Depends(get_current_user),
|
| 463 |
db: AsyncSession = Depends(get_db)
|
| 464 |
):
|
| 465 |
"""
|
| 466 |
+
Get user's payment history with pagination.
|
| 467 |
|
| 468 |
+
Returns payment transactions ordered by newest first.
|
| 469 |
"""
|
| 470 |
+
# Get total count
|
| 471 |
+
count_result = await db.execute(
|
| 472 |
+
select(func.count(PaymentTransaction.id))
|
| 473 |
+
.where(PaymentTransaction.user_id == user.user_id)
|
| 474 |
+
)
|
| 475 |
+
total_count = count_result.scalar() or 0
|
| 476 |
+
|
| 477 |
+
# Calculate offset
|
| 478 |
+
offset = (page - 1) * limit
|
| 479 |
+
|
| 480 |
+
# Get paginated transactions
|
| 481 |
result = await db.execute(
|
| 482 |
select(PaymentTransaction)
|
| 483 |
.where(PaymentTransaction.user_id == user.user_id)
|
| 484 |
.order_by(desc(PaymentTransaction.created_at))
|
| 485 |
+
.offset(offset)
|
| 486 |
+
.limit(limit)
|
| 487 |
)
|
| 488 |
transactions = result.scalars().all()
|
| 489 |
|
|
|
|
| 491 |
for txn in transactions:
|
| 492 |
history.append(PaymentHistoryItem(
|
| 493 |
transaction_id=txn.transaction_id,
|
| 494 |
+
razorpay_payment_id=txn.gateway_payment_id,
|
| 495 |
package_id=txn.package_id,
|
| 496 |
credits_amount=txn.credits_amount,
|
| 497 |
amount_paise=txn.amount_paise,
|
|
|
|
| 505 |
|
| 506 |
return PaymentHistoryResponse(
|
| 507 |
transactions=history,
|
| 508 |
+
total_count=total_count,
|
| 509 |
+
page=page,
|
| 510 |
+
limit=limit
|
| 511 |
)
|