Summary
Editing an existing quota assignment in the Admin Dashboard (Admin → Quotas → Edit Assignment) fails with a 400 Bad Request:
Invalid Request
"QuotaAssignment" object has no field "tierId"
Status: 400 Bad Request
URL: /api/admin/quota/assignments/{assignment_id}
Updating an App Role quota assignment (changing the Quota Tier and/or Priority, then clicking Update Assignment) is currently broken.
Steps to Reproduce
- Go to Admin → Quotas.
- Edit an existing assignment (e.g. an App Role assignment for
Faculty).
- Change the Quota Tier (e.g. to
Faculty ($30.00/month)) and/or the Priority (e.g. 500).
- Click Update Assignment.
Expected: The assignment is updated and saved.
Actual: 400 Bad Request — "QuotaAssignment" object has no field "tierId". No update is persisted.
Root Cause
There is an alias/field-name mismatch between the admin service and the repository update path.
1. Service dumps the update payload using aliases (camelCase):
backend/src/apis/app_api/admin/quota/service.py — QuotaAdminService.update_assignment
# Convert to dict and filter None values
update_dict = updates.model_dump(by_alias=True, exclude_none=True) # -> {"tierId": ..., "priority": ..., "enabled": ...}
updated = await self.repository.update_assignment(assignment_id, update_dict)
2. Repository applies those keys via setattr on a Pydantic model that only has snake_case fields:
backend/src/agents/main_agent/quota/repository.py — QuotaRepository.update_assignment
# Apply updates to current assignment
for key, value in updates.items():
setattr(current, key, value) # setattr(current, "tierId", ...) -> raises ValueError
current is a QuotaAssignment whose field is tier_id (alias tierId). In Pydantic v2, setattr(model, "tierId", ...) raises:
"QuotaAssignment" object has no field "tierId"
This matches the observed error exactly. Note that priority and enabled have no alias (key == field name), so they succeed — only tierId triggers the failure, which is why editing the tier is what breaks.
The underlying conflict
repository.update_assignment reuses the same dict keys for two incompatible purposes:
setattr(current, key, value) — requires snake_case field names (tier_id).
- Building the DynamoDB
UpdateExpression / ExpressionAttributeNames — must match the stored attribute names, which are the aliased camelCase keys (tierId), because items are written with assignment.model_dump(by_alias=True) in both create_assignment and at the end of update_assignment.
So simply flipping the service to by_alias=False would fix the setattr call but then write a mismatched tier_id attribute to DynamoDB alongside the existing tierId, corrupting the record.
Suggested Fix
Decouple the two concerns in repository.update_assignment: use field names for setattr, and derive the aliased attribute names for the DynamoDB update expression from the model. For example:
- Pass field-name keys from the service (
updates.model_dump(by_alias=False, exclude_none=True)), and
- In the repository, after applying
setattr, build the update expression from the aliased serialization of only the changed fields (e.g. re-serialize current with by_alias=True and include={...changed fields...}, plus the rebuilt GSI keys), so stored attributes stay consistent (tierId, assignmentType, etc.).
Whichever direction is chosen, the setattr keys and the DynamoDB attribute keys must be handled separately rather than shared.
Affected Files
backend/src/apis/app_api/admin/quota/service.py (update_assignment)
backend/src/agents/main_agent/quota/repository.py (update_assignment)
Notes
- A regression test should cover updating an assignment's
tier_id and asserting the stored DynamoDB item keeps camelCase attribute names with no duplicate tier_id/tierId.
- Reported from
boisestate.ai Admin UI; environment shows the same code paths as this repo.
Summary
Editing an existing quota assignment in the Admin Dashboard (Admin → Quotas → Edit Assignment) fails with a
400 Bad Request:Updating an App Role quota assignment (changing the Quota Tier and/or Priority, then clicking Update Assignment) is currently broken.
Steps to Reproduce
Faculty).Faculty ($30.00/month)) and/or the Priority (e.g.500).Expected: The assignment is updated and saved.
Actual:
400 Bad Request—"QuotaAssignment" object has no field "tierId". No update is persisted.Root Cause
There is an alias/field-name mismatch between the admin service and the repository update path.
1. Service dumps the update payload using aliases (camelCase):
backend/src/apis/app_api/admin/quota/service.py—QuotaAdminService.update_assignment2. Repository applies those keys via
setattron a Pydantic model that only has snake_case fields:backend/src/agents/main_agent/quota/repository.py—QuotaRepository.update_assignmentcurrentis aQuotaAssignmentwhose field istier_id(aliastierId). In Pydantic v2,setattr(model, "tierId", ...)raises:This matches the observed error exactly. Note that
priorityandenabledhave no alias (key == field name), so they succeed — onlytierIdtriggers the failure, which is why editing the tier is what breaks.The underlying conflict
repository.update_assignmentreuses the same dict keys for two incompatible purposes:setattr(current, key, value)— requires snake_case field names (tier_id).UpdateExpression/ExpressionAttributeNames— must match the stored attribute names, which are the aliased camelCase keys (tierId), because items are written withassignment.model_dump(by_alias=True)in bothcreate_assignmentand at the end ofupdate_assignment.So simply flipping the service to
by_alias=Falsewould fix thesetattrcall but then write a mismatchedtier_idattribute to DynamoDB alongside the existingtierId, corrupting the record.Suggested Fix
Decouple the two concerns in
repository.update_assignment: use field names forsetattr, and derive the aliased attribute names for the DynamoDB update expression from the model. For example:updates.model_dump(by_alias=False, exclude_none=True)), andsetattr, build the update expression from the aliased serialization of only the changed fields (e.g. re-serializecurrentwithby_alias=Trueandinclude={...changed fields...}, plus the rebuilt GSI keys), so stored attributes stay consistent (tierId,assignmentType, etc.).Whichever direction is chosen, the
setattrkeys and the DynamoDB attribute keys must be handled separately rather than shared.Affected Files
backend/src/apis/app_api/admin/quota/service.py(update_assignment)backend/src/agents/main_agent/quota/repository.py(update_assignment)Notes
tier_idand asserting the stored DynamoDB item keeps camelCase attribute names with no duplicatetier_id/tierId.boisestate.aiAdmin UI; environment shows the same code paths as this repo.