Skip to content

Commit 60e47f7

Browse files
committed
Add pricing tables API functionality
- Introduced a new `pricing_tables` module with endpoints for creating, retrieving, and deleting pricing tables, accessible based on user roles. - Updated the main application to include the new pricing tables router. - Added corresponding schemas for pricing table creation and response. - Implemented tests to validate the functionality and access control for pricing table operations.
1 parent c26cf24 commit 60e47f7

File tree

4 files changed

+339
-1
lines changed

4 files changed

+339
-1
lines changed

app/api/pricing_tables.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from fastapi import APIRouter, Depends, HTTPException, status
2+
from sqlalchemy.orm import Session
3+
from datetime import datetime, UTC
4+
5+
from app.db.database import get_db
6+
from app.db.models import DBSystemSecret
7+
from app.core.security import check_system_admin, get_role_min_team_admin
8+
from app.schemas.models import PricingTableCreate, PricingTableResponse
9+
10+
router = APIRouter(
11+
tags=["pricing-tables"]
12+
)
13+
14+
@router.post("", response_model=PricingTableResponse, status_code=status.HTTP_201_CREATED, dependencies=[Depends(check_system_admin)])
15+
@router.post("/", response_model=PricingTableResponse, status_code=status.HTTP_201_CREATED, dependencies=[Depends(check_system_admin)])
16+
async def create_pricing_table(
17+
pricing_table: PricingTableCreate,
18+
db: Session = Depends(get_db)
19+
):
20+
"""
21+
Create or update the current pricing table. Only accessible by system admin users.
22+
There can only be one active pricing table at a time.
23+
"""
24+
# Check if a pricing table already exists
25+
existing_table = db.query(DBSystemSecret).filter(DBSystemSecret.key == "CurrentPricingTable").first()
26+
27+
if existing_table:
28+
# Update existing table
29+
existing_table.value = pricing_table.pricing_table_id
30+
existing_table.updated_at = datetime.now(UTC)
31+
db.commit()
32+
db.refresh(existing_table)
33+
return PricingTableResponse(
34+
pricing_table_id=existing_table.value,
35+
updated_at=existing_table.updated_at
36+
)
37+
else:
38+
# Create new table
39+
db_table = DBSystemSecret(
40+
key="CurrentPricingTable",
41+
value=pricing_table.pricing_table_id,
42+
description="Current Stripe pricing table ID",
43+
created_at=datetime.now(UTC)
44+
)
45+
db.add(db_table)
46+
db.commit()
47+
db.refresh(db_table)
48+
return PricingTableResponse(
49+
pricing_table_id=db_table.value,
50+
updated_at=db_table.created_at
51+
)
52+
53+
@router.get("", response_model=PricingTableResponse, dependencies=[Depends(get_role_min_team_admin)])
54+
@router.get("/", response_model=PricingTableResponse, dependencies=[Depends(get_role_min_team_admin)])
55+
async def get_pricing_table(
56+
db: Session = Depends(get_db)
57+
):
58+
"""
59+
Get the current pricing table ID. Only accessible by team admin users or higher privileges.
60+
"""
61+
pricing_table = db.query(DBSystemSecret).filter(DBSystemSecret.key == "CurrentPricingTable").first()
62+
if not pricing_table:
63+
raise HTTPException(
64+
status_code=status.HTTP_404_NOT_FOUND,
65+
detail="No pricing table found"
66+
)
67+
return PricingTableResponse(
68+
pricing_table_id=pricing_table.value,
69+
updated_at=pricing_table.updated_at or pricing_table.created_at
70+
)
71+
72+
@router.delete("", dependencies=[Depends(check_system_admin)])
73+
@router.delete("/", dependencies=[Depends(check_system_admin)])
74+
async def delete_pricing_table(
75+
db: Session = Depends(get_db)
76+
):
77+
"""
78+
Delete the current pricing table. Only accessible by system admin users.
79+
"""
80+
pricing_table = db.query(DBSystemSecret).filter(DBSystemSecret.key == "CurrentPricingTable").first()
81+
if not pricing_table:
82+
raise HTTPException(
83+
status_code=status.HTTP_404_NOT_FOUND,
84+
detail="No pricing table found"
85+
)
86+
87+
db.delete(pricing_table)
88+
db.commit()
89+
90+
return {"message": "Pricing table deleted successfully"}

app/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from fastapi.openapi.docs import get_swagger_ui_html
66
from fastapi.openapi.utils import get_openapi
77
from prometheus_fastapi_instrumentator import Instrumentator, metrics
8-
from app.api import auth, private_ai_keys, users, regions, audit, teams, billing, products
8+
from app.api import auth, private_ai_keys, users, regions, audit, teams, billing, products, pricing_tables
99
from app.core.config import settings
1010
from app.db.database import get_db
1111
from app.middleware.audit import AuditLogMiddleware
@@ -134,6 +134,7 @@ async def health_check():
134134
app.include_router(teams.router, prefix="/teams", tags=["teams"])
135135
app.include_router(billing.router, prefix="/billing", tags=["billing"])
136136
app.include_router(products.router, prefix="/products", tags=["products"])
137+
app.include_router(pricing_tables.router, prefix="/pricing-tables", tags=["pricing-tables"])
137138

138139
@app.get("/", include_in_schema=False)
139140
async def custom_swagger_ui_html():

app/schemas/models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,4 +311,13 @@ class Product(ProductBase):
311311

312312
class PricingTableSession(BaseModel):
313313
client_secret: str
314+
model_config = ConfigDict(from_attributes=True)
315+
316+
class PricingTableCreate(BaseModel):
317+
pricing_table_id: str
318+
model_config = ConfigDict(from_attributes=True)
319+
320+
class PricingTableResponse(BaseModel):
321+
pricing_table_id: str
322+
updated_at: datetime
314323
model_config = ConfigDict(from_attributes=True)

tests/test_pricing_tables.py

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import pytest
2+
from fastapi import status
3+
from datetime import datetime, UTC
4+
from app.db.models import DBSystemSecret, DBUser
5+
from app.core.security import get_password_hash
6+
7+
def test_create_pricing_table_system_admin(client, db, test_admin):
8+
"""Test that a system admin can create a pricing table"""
9+
# Login as system admin
10+
response = client.post(
11+
"/auth/login",
12+
data={"username": test_admin.email, "password": "adminpassword"}
13+
)
14+
assert response.status_code == status.HTTP_200_OK
15+
token = response.json()["access_token"]
16+
headers = {"Authorization": f"Bearer {token}"}
17+
18+
# Create pricing table
19+
response = client.post(
20+
"/pricing-tables",
21+
json={"pricing_table_id": "test_pricing_table_123"},
22+
headers=headers
23+
)
24+
assert response.status_code == status.HTTP_201_CREATED
25+
data = response.json()
26+
assert data["pricing_table_id"] == "test_pricing_table_123"
27+
assert "updated_at" in data
28+
29+
# Verify in database
30+
pricing_table = db.query(DBSystemSecret).filter(
31+
DBSystemSecret.key == "CurrentPricingTable"
32+
).first()
33+
assert pricing_table is not None
34+
assert pricing_table.value == "test_pricing_table_123"
35+
36+
def test_create_pricing_table_team_admin(client, db, test_team_admin):
37+
"""Test that a team admin cannot create a pricing table"""
38+
# Login as team admin
39+
response = client.post(
40+
"/auth/login",
41+
data={"username": test_team_admin.email, "password": "password123"}
42+
)
43+
assert response.status_code == status.HTTP_200_OK
44+
token = response.json()["access_token"]
45+
headers = {"Authorization": f"Bearer {token}"}
46+
47+
# Try to create pricing table
48+
response = client.post(
49+
"/pricing-tables",
50+
json={"pricing_table_id": "test_pricing_table_123"},
51+
headers=headers
52+
)
53+
assert response.status_code == status.HTTP_403_FORBIDDEN
54+
55+
def test_get_pricing_table_team_admin(client, db, test_team_admin):
56+
"""Test that a team admin can get the pricing table"""
57+
# Create pricing table as system admin
58+
system_admin = DBUser(
59+
email="system_admin@test.com",
60+
hashed_password=get_password_hash("testpassword"),
61+
is_admin=True
62+
)
63+
db.add(system_admin)
64+
db.commit()
65+
66+
# Create pricing table
67+
pricing_table = DBSystemSecret(
68+
key="CurrentPricingTable",
69+
value="test_pricing_table_123",
70+
description="Test pricing table",
71+
created_at=datetime.now(UTC)
72+
)
73+
db.add(pricing_table)
74+
db.commit()
75+
76+
# Login as team admin
77+
response = client.post(
78+
"/auth/login",
79+
data={"username": test_team_admin.email, "password": "password123"}
80+
)
81+
assert response.status_code == status.HTTP_200_OK
82+
token = response.json()["access_token"]
83+
headers = {"Authorization": f"Bearer {token}"}
84+
85+
# Get pricing table
86+
response = client.get("/pricing-tables", headers=headers)
87+
assert response.status_code == status.HTTP_200_OK
88+
data = response.json()
89+
assert data["pricing_table_id"] == "test_pricing_table_123"
90+
assert "updated_at" in data
91+
92+
def test_get_pricing_table_not_found(client, db, test_team_admin):
93+
"""Test getting pricing table when none exists"""
94+
# Login as team admin
95+
response = client.post(
96+
"/auth/login",
97+
data={"username": test_team_admin.email, "password": "password123"}
98+
)
99+
assert response.status_code == status.HTTP_200_OK
100+
token = response.json()["access_token"]
101+
headers = {"Authorization": f"Bearer {token}"}
102+
103+
# Try to get pricing table
104+
response = client.get("/pricing-tables", headers=headers)
105+
assert response.status_code == status.HTTP_404_NOT_FOUND
106+
107+
def test_delete_pricing_table_system_admin(client, db, test_admin):
108+
"""Test that a system admin can delete the pricing table"""
109+
# Create pricing table
110+
pricing_table = DBSystemSecret(
111+
key="CurrentPricingTable",
112+
value="test_pricing_table_123",
113+
description="Test pricing table",
114+
created_at=datetime.now(UTC)
115+
)
116+
db.add(pricing_table)
117+
db.commit()
118+
119+
# Login as system admin
120+
response = client.post(
121+
"/auth/login",
122+
data={"username": test_admin.email, "password": "adminpassword"}
123+
)
124+
assert response.status_code == status.HTTP_200_OK
125+
token = response.json()["access_token"]
126+
headers = {"Authorization": f"Bearer {token}"}
127+
128+
# Delete pricing table
129+
response = client.delete("/pricing-tables", headers=headers)
130+
assert response.status_code == status.HTTP_200_OK
131+
assert response.json()["message"] == "Pricing table deleted successfully"
132+
133+
# Verify deleted from database
134+
pricing_table = db.query(DBSystemSecret).filter(
135+
DBSystemSecret.key == "CurrentPricingTable"
136+
).first()
137+
assert pricing_table is None
138+
139+
def test_delete_pricing_table_team_admin(client, db, test_team_admin):
140+
"""Test that a team admin cannot delete the pricing table"""
141+
# Create pricing table
142+
pricing_table = DBSystemSecret(
143+
key="CurrentPricingTable",
144+
value="test_pricing_table_123",
145+
description="Test pricing table",
146+
created_at=datetime.now(UTC)
147+
)
148+
db.add(pricing_table)
149+
db.commit()
150+
151+
# Login as team admin
152+
response = client.post(
153+
"/auth/login",
154+
data={"username": test_team_admin.email, "password": "password123"}
155+
)
156+
assert response.status_code == status.HTTP_200_OK
157+
token = response.json()["access_token"]
158+
headers = {"Authorization": f"Bearer {token}"}
159+
160+
# Try to delete pricing table
161+
response = client.delete("/pricing-tables", headers=headers)
162+
assert response.status_code == status.HTTP_403_FORBIDDEN
163+
164+
def test_update_existing_pricing_table(client, db, test_admin):
165+
"""Test updating an existing pricing table"""
166+
# Create initial pricing table
167+
pricing_table = DBSystemSecret(
168+
key="CurrentPricingTable",
169+
value="initial_pricing_table",
170+
description="Test pricing table",
171+
created_at=datetime.now(UTC)
172+
)
173+
db.add(pricing_table)
174+
db.commit()
175+
176+
# Login as system admin
177+
response = client.post(
178+
"/auth/login",
179+
data={"username": test_admin.email, "password": "adminpassword"}
180+
)
181+
assert response.status_code == status.HTTP_200_OK
182+
token = response.json()["access_token"]
183+
headers = {"Authorization": f"Bearer {token}"}
184+
185+
# Update pricing table
186+
response = client.post(
187+
"/pricing-tables",
188+
json={"pricing_table_id": "updated_pricing_table"},
189+
headers=headers
190+
)
191+
assert response.status_code == status.HTTP_201_CREATED
192+
data = response.json()
193+
assert data["pricing_table_id"] == "updated_pricing_table"
194+
assert "updated_at" in data
195+
196+
# Verify only one pricing table exists with updated value
197+
pricing_tables = db.query(DBSystemSecret).filter(
198+
DBSystemSecret.key == "CurrentPricingTable"
199+
).all()
200+
assert len(pricing_tables) == 1
201+
assert pricing_tables[0].value == "updated_pricing_table"
202+
203+
def test_key_creator_cannot_access_pricing_table(client, db, test_team_key_creator):
204+
"""Test that a key_creator cannot access the pricing table"""
205+
# Create pricing table as system admin
206+
pricing_table = DBSystemSecret(
207+
key="CurrentPricingTable",
208+
value="test_pricing_table_123",
209+
description="Test pricing table",
210+
created_at=datetime.now(UTC)
211+
)
212+
db.add(pricing_table)
213+
db.commit()
214+
215+
# Login as key_creator
216+
response = client.post(
217+
"/auth/login",
218+
data={"username": test_team_key_creator.email, "password": "password123"}
219+
)
220+
assert response.status_code == status.HTTP_200_OK
221+
token = response.json()["access_token"]
222+
headers = {"Authorization": f"Bearer {token}"}
223+
224+
# Try to get pricing table
225+
response = client.get("/pricing-tables", headers=headers)
226+
assert response.status_code == status.HTTP_403_FORBIDDEN
227+
228+
# Try to create pricing table
229+
response = client.post(
230+
"/pricing-tables",
231+
json={"pricing_table_id": "new_pricing_table"},
232+
headers=headers
233+
)
234+
assert response.status_code == status.HTTP_403_FORBIDDEN
235+
236+
# Try to delete pricing table
237+
response = client.delete("/pricing-tables", headers=headers)
238+
assert response.status_code == status.HTTP_403_FORBIDDEN

0 commit comments

Comments
 (0)