Budget Approval Request Flow
This feature allows team members to request budget overrides when they exceed their monthly limits, requiring admin approval for temporary budget increases.
Architecture
Database Model
**Table:** budget_approval_requests
Located in /Users/rushiparikh/projects/atom-saas/backend-saas/core/models.py
class BudgetApprovalRequest(Base):
id = Column(String, primary_key=True)
tenant_id = Column(String, ForeignKey("tenants.id"))
requester_id = Column(String) # User who made the request
reason = Column(Text) # Explanation for the override
additional_budget_requested = Column(Float) # USD amount
status = Column(String(20)) # "pending", "approved", "denied"
reviewer_id = Column(String) # Admin who reviewed
reviewed_at = Column(DateTime)
reviewer_notes = Column(Text)
override_expires_at = Column(DateTime) # When the approved override expires
created_at = Column(DateTime)
updated_at = Column(DateTime)API Endpoints
All endpoints are in /Users/rushiparikh/projects/atom-saas/backend-saas/api/routes/billing_routes.py
1. Create Approval Request
**POST** /api/billing/budget/approval-request
Requires: Admin/Owner role
const response = await fetch('/api/billing/budget/approval-request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
reason: 'Need additional budget for critical client project',
additional_budget_requested: 100.00
})
})**Response:**
{
"request_id": "uuid-123",
"status": "pending",
"created_at": "2026-04-05T21:22:45Z"
}**Email Notifications:** Sent to all workspace admins (excluding requester)
2. List Approval Requests
**GET** /api/billing/budget/approval-requests?status=pending
- Admins see all requests
- Non-admins see only their own requests
- Optional filter:
?status=pending|approved|denied
**Response:**
[
{
"id": "uuid-123",
"requester_id": "user-456",
"reason": "Need additional budget for critical client project",
"additional_budget_requested": 100.00,
"status": "pending",
"reviewed_at": null,
"reviewer_notes": null,
"override_expires_at": null,
"created_at": "2026-04-05T21:22:45Z"
}
]3. Approve Request
**POST** /api/billing/budget/approval-request/{request_id}/approve
Requires: Admin role
const response = await fetch(`/api/billing/budget/approval-request/${requestId}/approve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
reviewer_notes: "Approved for Q1 project",
override_duration_hours: 24 // Default: 24, Max: 168 (1 week)
})
})**Response:**
{
"success": true,
"override_expires_at": "2026-04-06T21:22:45Z",
"duration_hours": 24
}**Email Notification:** Sent to requester
4. Deny Request
**POST** /api/billing/budget/approval-request/{request_id}/deny
Requires: Admin role
const response = await fetch(`/api/billing/budget/approval-request/${requestId}/deny`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
reviewer_notes: "Budget already exceeded for this month"
})
})**Response:**
{
"success": true
}**Email Notification:** Sent to requester
Frontend Component
**Location:** /Users/rushiparikh/projects/atom-saas/src/components/billing/ApprovalRequestDialog.tsx
**Features:**
- Dual mode: Create request + List requests
- Admin actions: Approve/Deny with notes
- Auto-refresh every 30 seconds for pending requests
- Form validation (reason length, budget amount)
- Status badges (pending/approved/denied)
- Override duration configuration (1-168 hours)
**Usage:**
import { ApprovalRequestDialog } from '@/components/billing/ApprovalRequestDialog'
function BudgetPage() {
const [isOpen, setIsOpen] = useState(false)
const isAdmin = session?.user?.role === 'admin'
return (
<>
<Button onClick={() => setIsOpen(true)}>
Request Budget Override
</Button>
<ApprovalRequestDialog
open={isOpen}
onOpenChange={setIsOpen}
isAdmin={isAdmin}
onSuccess={() => {
// Refresh budget status
mutate()
}}
/>
</>
)
}Integration with Budget Exceeded Banner
The approval request dialog integrates seamlessly with the existing BudgetExceededBanner component:
import { BudgetExceededBanner } from '@/components/billing/BudgetExceededBanner'
import { ApprovalRequestDialog } from '@/components/billing/ApprovalRequestDialog'
<BudgetExceededBanner
currentPlan={plan}
currentSpend={spend}
budgetLimit={limit}
utilizationPercent={percent}
onUpgrade={() => router.push('/settings/billing')}
onIncreaseBudget={() => setApprovalDialogOpen(true)}
/>
<ApprovalRequestDialog
open={approvalDialogOpen}
onOpenChange={setApprovalDialogOpen}
isAdmin={isAdmin}
onSuccess={() => mutate()}
/>Email Templates
1. Request Notification (to Admins)
**Subject:** 🔔 Budget Approval Requested - user@example.com
**Content:**
- Requester email
- Additional budget amount
- Reason for request
- Request ID
- Link to approval page
2. Approval Notification (to Requester)
**Subject:** ✅ Budget Override Approved
**Content:**
- Additional budget amount
- Override expiration date/time
- Duration of override
- Admin notes (if provided)
3. Denial Notification (to Requester)
**Subject:** ❌ Budget Override Request Denied
**Content:**
- Requested amount
- Original reason
- Admin notes explaining denial
Security & Permissions
Create Request
- **Role Required:** Admin, Owner, Super Admin
- **Validation:**
additional_budget_requested > 0reason.length >= 10characters
Review (Approve/Deny)
- **Role Required:** Admin, Owner, Super Admin
- **Validation:**
- Can only review pending requests
- Override duration: 1-168 hours
- Denial requires reviewer notes
List Requests
- **Admins:** See all tenant requests
- **Non-Admins:** See only their own requests
Database Migration
**Migration File:** /Users/rushiparikh/projects/atom-saas/backend-saas/alembic/versions/20260405_212245_ec6a654117b0.py
**Status:** Applied (stamped)
**Table Created:** budget_approval_requests
**Indexes:**
ix_budget_approval_requests_id(primary)ix_budget_approval_requests_tenant_idix_budget_approval_requests_requester_idix_budget_approval_requests_statusidx_budget_approval_tenant_status(composite)idx_budget_approval_created
Testing
Manual Testing Steps
- **Create Request:**
- **List Requests:**
- **Approve Request (Admin):**
- **Deny Request (Admin):**
Future Enhancements
- **Multi-step Approval:** Require multiple admins for large amounts
- **Auto-approval Rules:** Pre-approve requests under certain conditions
- **Request Templates:** Common reasons with pre-filled descriptions
- **Approval Analytics:** Track approval rates, average times, amounts
- **Budget Forecasting:** Show impact of approval on monthly budget
- **SLA Notifications:** Alert if request pending > X hours
Troubleshooting
Email Not Sending
Check email service configuration:
# Backend
echo $EMAIL_PROVIDER # smtp, sendgrid, or ses
echo $EMAIL_FROM_ADDRESSMigration Issues
If migration fails, check table exists:
from core.database import engine
import sqlalchemy as sa
inspector = sa.inspect(engine)
print(inspector.get_table_names())Permission Errors
Verify user role:
console.log(session?.user?.role) // Should be "admin", "owner", or "super_admin"Files Modified
/Users/rushiparikh/projects/atom-saas/backend-saas/core/models.py
- Added
BudgetApprovalRequestmodel - Fixed
PricingFetchLogindexes
/Users/rushiparikh/projects/atom-saas/backend-saas/api/routes/billing_routes.py
- Added 4 new endpoints
- Email notification integration
/Users/rushiparikh/projects/atom-saas/backend-saas/alembic/versions/20260405_212245_ec6a654117b0.py
- Database migration
/Users/rushiparikh/projects/atom-saas/src/components/billing/ApprovalRequestDialog.tsx
- New React component
/Users/rushiparikh/projects/atom-saas/backend-saas/alembic/versions/20260405_063936_c1a5813cfd35.py
- Fixed missing imports (String, Text)
Deployment Checklist
- [ ] Database migration applied
- [ ] Email service configured (SMTP/SendGrid/SES)
- [ ] FRONTEND_URL environment variable set
- [ ] Test with real admin accounts
- [ ] Verify email notifications work
- [ ] Check budget enforcement respects approved overrides
- [ ] Monitor approval request volume
Support
For issues or questions:
- Check logs:
/Users/rushiparikh/projects/atom-saas/backend-saas/logs/ - Review database:
SELECT * FROM budget_approval_requests ORDER BY created_at DESC LIMIT 10; - Test email service: Use
core.email_service.email_service.send_email()directly