Skip to content

Editing a quota assignment fails: "QuotaAssignment" object has no field "tierId" (400) #718

Description

@DerrickF

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

  1. Go to Admin → Quotas.
  2. Edit an existing assignment (e.g. an App Role assignment for Faculty).
  3. Change the Quota Tier (e.g. to Faculty ($30.00/month)) and/or the Priority (e.g. 500).
  4. 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.pyQuotaAdminService.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.pyQuotaRepository.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:

  1. setattr(current, key, value) — requires snake_case field names (tier_id).
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions