Repository: aipotheosis-labs/aci
Pull request #354: Feat/linked accounts quota
Author: login: alex-aipolabs
Created: 2025-05-12T17:42:57Z | Merged: 2025-05-19T13:50:16Z

### 🏷️ Ticket

https://www.notion.so/Enforce-Rate-Limiting-for-Pricing-1f18378d6a4780e39d2bee63d33f38fa?pvs=4

### 📝 Description

Enforce quota for linked account owner ids

### 🎥 Demo (if applicable)

### 📸 Screenshots (if applicable)

### ✅ Checklist

- [x] I have signed the [Contributor License Agreement]() (CLA) and read the [contributing guide](./../CONTRIBUTING.md) (required)
- [x] I have linked this PR to an issue or a ticket (required)
- [ ] I have updated the documentation related to my change if needed
- [x] I have updated the tests accordingly (required for a bug fix or a new feature)
- [x] All checks on CI passed


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Enforced linked account creation limits per organization based on subscription plans.
  - Added clear error notifications when the maximum number of unique linked account owner IDs is reached.
- **Tests**
  - Added tests to verify linked account quota enforcement across multiple projects and app configurations within an organization.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/backend/aci/common/db/crud/linked_accounts.py b/backend/aci/common/db/crud/linked_accounts.py
index 173876827..4ceb9a84f 100644
--- a/backend/aci/common/db/crud/linked_accounts.py
+++ b/backend/aci/common/db/crud/linked_accounts.py
@@ -1,11 +1,11 @@
 from datetime import datetime
 from uuid import UUID
 
-from sqlalchemy import select
+from sqlalchemy import distinct, func, select
 from sqlalchemy.orm import Session
 
 from aci.common import validators
-from aci.common.db.sql_models import App, LinkedAccount
+from aci.common.db.sql_models import App, LinkedAccount, Project
 from aci.common.enums import SecurityScheme
 from aci.common.logging_setup import get_logger
 from aci.common.schemas.linked_accounts import LinkedAccountUpdate
@@ -166,3 +166,32 @@ def delete_linked_accounts(db_session: Session, project_id: UUID, app_name: str)
         db_session.delete(linked_account)
     db_session.flush()
     return len(linked_accounts_to_delete)
+
+
+def get_total_number_of_unique_linked_account_owner_ids(db_session: Session, org_id: UUID) -> int:
+    """
+    TODO: Add a lock to prevent the race condition.
+    Get the total number of unique linked account owner IDs for an organization.
+
+    WARNING: Race condition potential! This function is vulnerable to race conditions in
+    concurrent environments. If this function is called concurrently with operations that
+    add or remove linked accounts:
+
+    1. Thread A starts counting unique linked_account_owner_ids
+    2. Thread B adds a new linked account with a new owner_id
+    3. Thread A completes its count, unaware of the newly added account
+    """
+    statement = select(func.count(distinct(LinkedAccount.linked_account_owner_id))).where(
+        LinkedAccount.project_id.in_(select(Project.id).filter(Project.org_id == org_id))
+    )
+    return db_session.execute(statement).scalar_one()
+
+
+def linked_account_owner_id_exists_in_org(
+    db_session: Session, org_id: UUID, linked_account_owner_id: str
+) -> bool:
+    statement = select(LinkedAccount).filter(
+        LinkedAccount.linked_account_owner_id == linked_account_owner_id,
+        LinkedAccount.project_id.in_(select(Project.id).filter(Project.org_id == org_id)),
+    )
+    return db_session.execute(statement).scalar_one_or_none() is not None
diff --git a/backend/aci/common/exceptions.py b/backend/aci/common/exceptions.py
index d95002e54..ee7a8bb26 100644
--- a/backend/aci/common/exceptions.py
+++ b/backend/aci/common/exceptions.py
@@ -432,3 +432,14 @@ def __init__(self, message: str | None = None):
             message=message,
             error_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
         )
+
+
+class MaxUniqueLinkedAccountOwnerIdsReached(ACIException):
+    """Raised when an organization has reached its maximum allowed linked accounts for an app."""
+
+    def __init__(self, message: str | None = None):
+        super().__init__(
+            title="Max unique linked account owner ids reached",
+            message=message,
+            error_code=status.HTTP_403_FORBIDDEN,
+        )
diff --git a/backend/aci/server/quota_manager.py b/backend/aci/server/quota_manager.py
index 6e685163e..78552cf0a 100644
--- a/backend/aci/server/quota_manager.py
+++ b/backend/aci/server/quota_manager.py
@@ -11,7 +11,12 @@
 from sqlalchemy.orm import Session
 
 from aci.common.db import crud
-from aci.common.exceptions import MaxAgentsReached, MaxProjectsReached
+from aci.common.exceptions import (
+    MaxAgentsReached,
+    MaxProjectsReached,
+    MaxUniqueLinkedAccountOwnerIdsReached,
+    SubscriptionPlanNotFound,
+)
 from aci.common.logging_setup import get_logger
 from aci.server import config
 
@@ -64,3 +69,60 @@ def enforce_agent_creation_quota(db_session: Session, project_id: UUID) -> None:
             },
         )
         raise MaxAgentsReached()
+
+
+def enforce_linked_accounts_creation_quota(
+    db_session: Session, org_id: UUID, linked_account_owner_id: str
+) -> None:
+    """
+    Check and enforce that the organization doesn't have a unique_account_owner_id exceeding the
+    quota determined by the organization's current subscription plan.
+
+    Args:
+        db_session: Database session
+        org_id: ID of the organization to check
+        linked_account_owner_id: ID of the linked account owner to check
+
+    Raises:
+        MaxUniqueLinkedAccountOwnerIdsReached: If the organization has reached its maximum
+        allowed unique linked account owner ids
+        SubscriptionPlanNotFound: If the organization's subscription plan cannot be found
+    """
+    if crud.linked_accounts.linked_account_owner_id_exists_in_org(
+        db_session, org_id, linked_account_owner_id
+    ):
+        # If the linked account owner id already exists in the organization, linking this account
+        # will not increase the total number of unique linked account owner ids or exceed the quota.
+        return
+
+    # Get the organization's subscription
+    subscription = crud.subscriptions.get_subscription_by_org_id(db_session, org_id)
+    if not subscription:
+        # If no subscription found, use the free plan
+        plan = crud.plans.get_by_name(db_session, "free")
+        if not plan:
+            raise SubscriptionPlanNotFound("Free plan not found")
+    else:
+        # Get the plan from the subscription
+        plan = crud.plans.get_by_id(db_session, subscription.plan_id)
+        if not plan:
+            raise SubscriptionPlanNotFound(f"Plan {subscription.plan_id} not found")
+
+    # Get the linked accounts quota from the plan's features
+    max_unique_linked_account_owner_ids = plan.features.get("linked_accounts", 0)
+
+    num_unique_linked_account_owner_ids = (
+        crud.linked_accounts.get_total_number_of_unique_linked_account_owner_ids(db_session, org_id)
+    )
+
+    if num_unique_linked_account_owner_ids >= max_unique_linked_account_owner_ids:
+        logger.error(
+            "organization has reached maximum unique linked account owner ids quota for the current plan",
+            extra={
+                "org_id": org_id,
+                "max_unique_linked_account_owner_ids": max_unique_linked_account_owner_ids,
+                "num_unique_linked_account_owner_ids": num_unique_linked_account_owner_ids,
+                "plan": plan.name,
+            },
+        )
+        raise MaxUniqueLinkedAccountOwnerIdsReached()
diff --git a/backend/aci/server/routes/linked_accounts.py b/backend/aci/server/routes/linked_accounts.py
index 5a279c645..f1ded81ab 100644
--- a/backend/aci/server/routes/linked_accounts.py
+++ b/backend/aci/server/routes/linked_accounts.py
@@ -17,6 +17,7 @@
     LinkedAccountNotFound,
     NoImplementationFound,
     OAuth2Error,
+    ProjectNotFound,
 )
 from aci.common.logging_setup import get_logger
 from aci.common.schemas.linked_accounts import (
@@ -33,7 +34,7 @@
     APIKeySchemeCredentials,
     NoAuthSchemeCredentials,
 )
-from aci.server import config
+from aci.server import config, quota_manager
 from aci.server import dependencies as deps
 from aci.server import security_credentials_manager as scm
 from aci.server.oauth2_manager import OAuth2Manager
@@ -137,6 +138,11 @@ async def link_account_with_aci_default_credentials(
             f"linked account with linked_account_owner_id={body.linked_account_owner_id} already exists for app={body.app_name}"
         )
     else:
+        # Enforce linked accounts quota before creating new account
+        quota_manager.enforce_linked_accounts_creation_quota(
+            context.db_session, context.project.org_id, body.linked_account_owner_id
+        )
+
         logger.info(
             "creating linked account with ACI default credentials",
             extra={
@@ -211,6 +217,11 @@ async def link_account_with_no_auth(
             f"linked account with linked_account_owner_id={body.linked_account_owner_id} already exists for app={body.app_name}"
         )
     else:
+        # Enforce linked accounts quota before creating new account
+        quota_manager.enforce_linked_accounts_creation_quota(
+            context.db_session, context.project.org_id, body.linked_account_owner_id
+        )
+
         logger.info(
             "creating no_auth linked account",
             extra={
@@ -299,6 +310,11 @@ async def link_account_with_api_key(
             f"linked account with linked_account_owner_id={body.linked_account_owner_id} already exists for app={body.app_name}"
         )
     else:
+        # Enforce linked accounts quota before creating new account
+        quota_manager.enforce_linked_accounts_creation_quota(
+            context.db_session, context.project.org_id, body.linked_account_owner_id
+        )
+
         logger.info(
             "creating api_key linked account",
             extra={
@@ -363,6 +379,11 @@ async def link_oauth2_account(
             f"{app_configuration.security_scheme}, not OAuth2"
         )
 
+    # Enforce linked accounts quota before creating new account
+    quota_manager.enforce_linked_accounts_creation_quota(
+        context.db_session, context.project.org_id, query_params.linked_account_owner_id
+    )
+
     oauth2_scheme = scm.get_app_configuration_oauth2_scheme(
         app_configuration.app, app_configuration
     )
@@ -561,6 +582,20 @@ async def linked_accounts_oauth2_callback(
             db_session, linked_account, security_credentials
         )
     else:
+        # Get the organization ID from the project
+        project = crud.projects.get_project(db_session, state.project_id)
+        if not project:
+            logger.error(
+                "project not found when creating linked account",
+                extra={"project_id": state.project_id},
+            )
+            raise ProjectNotFound(f"Project with ID {state.project_id} not found")
+        org_id = project.org_id
+        # Enforce linked accounts quota before creating new account
+        quota_manager.enforce_linked_accounts_creation_quota(
+            db_session, org_id, state.linked_account_owner_id
+        )
+
         logger.info(
             "creating oauth2 linked account",
             extra={
diff --git a/backend/aci/server/tests/conftest.py b/backend/aci/server/tests/conftest.py
index 7e89441d8..f87c23538 100644
--- a/backend/aci/server/tests/conftest.py
+++ b/backend/aci/server/tests/conftest.py
@@ -15,7 +15,9 @@
 from sqlalchemy.engine.reflection import Inspector
 from sqlalchemy.orm import Session
 
+from aci.common.db.sql_models import Plan
 from aci.common.enums import OrganizationRole
+from aci.common.schemas.plans import PlanFeatures
 from aci.server import acl
 
 # override the rate limit to a high number for testing before importing aci modules
@@ -740,3 +742,25 @@ def dummy_linked_account_no_auth_mock_app_connector_project_1(
     )
     db_session.commit()
     yield dummy_linked_account_no_auth_mock_app_connector_project_1
+
+
+@pytest.fixture(scope="function")
+def free_plan(db_session: Session) -> Plan:
+    plan = crud.plans.create(
+        db=db_session,
+        name="free",
+        stripe_product_id="prod_FREE_placeholder",
+        stripe_monthly_price_id="price_FREE_monthly_placeholder",
+        stripe_yearly_price_id="price_FREE_yearly_placeholder",
+        features=PlanFeatures(
+            linked_accounts=3,
+            api_calls_monthly=1000,
+            agent_credentials=5,
+            developer_seats=1,
+            custom_oauth=False,
+            log_retention_days=7,
+        ),
+        is_public=True,
+    )
+    db_session.commit()
+    return plan
diff --git a/backend/aci/server/tests/routes/linked_acounts/test_accounts_quota.py b/backend/aci/server/tests/routes/linked_acounts/test_accounts_quota.py
new file mode 100644
index 000000000..57cdd2f94
--- /dev/null
+++ b/backend/aci/server/tests/routes/linked_acounts/test_accounts_quota.py
@@ -0,0 +1,150 @@
+from fastapi import status
+from fastapi.testclient import TestClient
+
+from aci.common.db.sql_models import Plan
+from aci.common.schemas.app_configurations import AppConfigurationPublic
+from aci.common.schemas.linked_accounts import (
+    LinkedAccountAPIKeyCreate,
+    LinkedAccountNoAuthCreate,
+    LinkedAccountOAuth2Create,
+)
+from aci.server import config
+
+
+def test_linked_accounts_quota_for_a_single_project_in_an_org(
+    test_client: TestClient,
+    dummy_api_key_1: str,
+    dummy_app_configuration_no_auth_mock_app_connector_project_1: AppConfigurationPublic,
+    dummy_app_configuration_api_key_aci_test_project_1: AppConfigurationPublic,
+    dummy_app_configuration_oauth2_google_project_1: AppConfigurationPublic,
+    free_plan: Plan,
+) -> None:
+    # Given: User has already created the max number of unique linked account owner ids
+    for i in range(free_plan.features["linked_accounts"]):
+        linked_account_owner_id = f"test_linked_accounts_quota_{i}"
+        body = LinkedAccountNoAuthCreate(
+            app_name=dummy_app_configuration_no_auth_mock_app_connector_project_1.app_name,
+            linked_account_owner_id=linked_account_owner_id,
+        )
+        response = test_client.post(
+            f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/no-auth",
+            json=body.model_dump(mode="json", exclude_none=True),
+            headers={"x-api-key": dummy_api_key_1},
+        )
+        assert response.status_code == status.HTTP_200_OK
+
+    # Then: User can still create a new linked account with the same owner id in a
+    # different app
+    existing_linked_account_owner_id = (
+        f"test_linked_accounts_quota_{free_plan.features['linked_accounts'] - 1}"
+    )
+    response = test_client.post(
+        f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/api-key",
+        json=LinkedAccountAPIKeyCreate(
+            app_name=dummy_app_configuration_api_key_aci_test_project_1.app_name,
+            linked_account_owner_id=existing_linked_account_owner_id,
+            api_key="aci_api_key",
+        ).model_dump(mode="json", exclude_none=True),
+        headers={"x-api-key": dummy_api_key_1},
+    )
+    assert response.status_code == status.HTTP_200_OK
+
+    # Then: User cannot create a new linked account with a new owner id in no-auth app
+    new_linked_account_owner_id = (
+        f"test_linked_accounts_quota_{free_plan.features['linked_accounts'] + 1}"
+    )
+    response = test_client.post(
+        f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/no-auth",
+        json=LinkedAccountNoAuthCreate(
+            app_name=dummy_app_configuration_no_auth_mock_app_connector_project_1.app_name,
+            linked_account_owner_id=new_linked_account_owner_id,
+        ).model_dump(mode="json", exclude_none=True),
+        headers={"x-api-key": dummy_api_key_1},
+    )
+    assert response.status_code == status.HTTP_403_FORBIDDEN
+
+    # Then: User cannot create a new linked account with a new owner id in api-key app
+    new_linked_account_owner_id = (
+        f"test_linked_accounts_quota_{free_plan.features['linked_accounts'] + 2}"
+    )
+    response = test_client.post(
+        f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/api-key",
+        json=LinkedAccountAPIKeyCreate(
+            app_name=dummy_app_configuration_api_key_aci_test_project_1.app_name,
+            linked_account_owner_id=new_linked_account_owner_id,
+            api_key="aci_api_key",
+        ).model_dump(mode="json", exclude_none=True),
+        headers={"x-api-key": dummy_api_key_1},
+    )
+    assert response.status_code == status.HTTP_403_FORBIDDEN
+
+    # Then: User cannot create a new linked account with a new owner id in oauth2 app
+    new_linked_account_owner_id = (
+        f"test_linked_accounts_quota_{free_plan.features['linked_accounts'] + 3}"
+    )
+    response = test_client.get(
+        f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/oauth2",
+        params=LinkedAccountOAuth2Create(
+            app_name=dummy_app_configuration_oauth2_google_project_1.app_name,
+            linked_account_owner_id=new_linked_account_owner_id,
+            after_oauth2_link_redirect_url="https://example.com",
+        ).model_dump(mode="json", exclude_none=True),
+        headers={"x-api-key": dummy_api_key_1},
+    )
+    assert response.status_code == status.HTTP_403_FORBIDDEN
+
+
+def test_linked_accounts_quota_for_multiple_projects_in_an_org(
+    test_client: TestClient,
+    dummy_api_key_1: str,
+    dummy_app_configuration_api_key_github_project_1: AppConfigurationPublic,
+    dummy_api_key_2: str,
+    dummy_app_configuration_api_key_github_project_2: AppConfigurationPublic,
+    free_plan: Plan,
+) -> None:
+    # Given: User has already created the max number of unique linked account owner ids
+    # in project 1
+    for i in range(free_plan.features["linked_accounts"]):
+        linked_account_owner_id = f"test_linked_accounts_quota_{i}"
+        body = LinkedAccountAPIKeyCreate(
+            app_name=dummy_app_configuration_api_key_github_project_1.app_name,
+            linked_account_owner_id=linked_account_owner_id,
+            api_key="aci_api_key",
+        )
+        response = test_client.post(
+            f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/api-key",
+            json=body.model_dump(mode="json", exclude_none=True),
+            headers={"x-api-key": dummy_api_key_1},
+        )
+        assert response.status_code == status.HTTP_200_OK
+
+    # Then: User can still create a new linked account with an existing owner id in
+    # project 2
+    existing_linked_account_owner_id = (
+        f"test_linked_accounts_quota_{free_plan.features['linked_accounts'] - 1}"
+    )
+    response = test_client.post(
+        f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/api-key",
+        json=LinkedAccountAPIKeyCreate(
+            app_name=dummy_app_configuration_api_key_github_project_2.app_name,
+            linked_account_owner_id=existing_linked_account_owner_id,
+            api_key="aci_api_key",
+        ).model_dump(mode="json", exclude_none=True),
+        headers={"x-api-key": dummy_api_key_2},
+    )
+    assert response.status_code == status.HTTP_200_OK
+
+    # Then: User cannot create a new linked account with a new owner id in project 2
+    new_linked_account_owner_id = (
+        f"test_linked_accounts_quota_{free_plan.features['linked_accounts'] + 1}"
+    )
+    response = test_client.post(
+        f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/api-key",
+        json=LinkedAccountAPIKeyCreate(
+            app_name=dummy_app_configuration_api_key_github_project_2.app_name,
+            linked_account_owner_id=new_linked_account_owner_id,
+            api_key="aci_api_key",
+        ).model_dump(mode="json", exclude_none=True),
+        headers={"x-api-key": dummy_api_key_2},
+    )
+    assert response.status_code == status.HTTP_403_FORBIDDEN


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>backend/aci/server/quota_manager.py (1)</summary><blockquote>

`75-132`: **Well-implemented quota enforcement function.**

The function follows the established pattern of other quota enforcement functions in this module. It properly:
1. Retrieves the project to get the organization ID
2. Checks the organization's subscription plan
3. Gets the linked accounts quota from the plan
4. Counts current linked accounts
5. Raises an exception if the quota is exceeded

A few considerations:

1. The fallback to the "free" plan (line 105) assumes it always exists. Consider adding a more robust fallback or clearer error message if the free plan doesn't exist.
2. The function uses `plan.features.get("linked_accounts", 0)` which safely handles the case when the feature isn't defined, defaulting to 0.
3. Error logging includes all relevant context, which is good practice.


Consider adding a comment explaining why the fallback to "free" plan is the appropriate behavior when no subscription exists:

```diff
    if not subscription:
        # If no subscription found, use the free plan
+       # Organizations without subscriptions default to free plan restrictions
        plan = crud.plans.get_by_name(db_session, "free")
```

</blockquote></details>
<details>
<summary>backend/aci/server/tests/routes/linked_acounts/test_accounts_quota.py (1)</summary><blockquote>

`1-72`: **Comprehensive test for linked accounts quota enforcement.**

The test effectively validates the quota enforcement by:
1. Creating a plan with a specific quota limit (2 linked accounts)
2. Successfully creating the allowed number of accounts
3. Verifying that attempts to exceed the quota are properly rejected
4. Checking both the status code and error message

The test provides good coverage of the happy path and limit enforcement scenarios.


Consider adding additional tests for edge cases:

```python
def test_linked_accounts_quota_with_missing_feature(
    test_client: TestClient,
    dummy_api_key_1: str,
    dummy_app_configuration_no_auth_mock_app_connector_project_1: AppConfigurationPublic,
    db_session: Session,
) -> None:
    # Create plan without linked_accounts feature to test graceful handling
    plan = Plan(
        name="no_linked_accounts_feature",
        stripe_product_id="prod_NO_FEATURE_placeholder",
        stripe_monthly_price_id="price_NO_FEATURE_monthly_placeholder",
        stripe_yearly_price_id="price_NO_FEATURE_yearly_placeholder",
        features={},  # No linked_accounts feature
        is_public=True,
    )
    db_session.add(plan)
    db_session.commit()
    
    # Attempt to create linked account should fail (quota of 0)
    body = LinkedAccountNoAuthCreate(
        app_name=dummy_app_configuration_no_auth_mock_app_connector_project_1.app_name,
        linked_account_owner_id="test_missing_feature",
    )
    response = test_client.post(
        f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/no-auth",
        json=body.model_dump(mode="json", exclude_none=True),
        headers={"x-api-key": dummy_api_key_1},
    )
    assert response.status_code == status.HTTP_403_FORBIDDEN
    assert "Max linked accounts reached" in str(response.json()["error"])
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 889e891fe6589834647f188002acca838806431c and f1181d34d098946939ee981463c4a1251b4d671d.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `backend/aci/common/exceptions.py` (1 hunks)
* `backend/aci/server/quota_manager.py` (2 hunks)
* `backend/aci/server/routes/linked_accounts.py` (5 hunks)
* `backend/aci/server/tests/routes/linked_acounts/test_accounts_quota.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>backend/aci/server/quota_manager.py (5)</summary><blockquote>

<details>
<summary>backend/aci/common/exceptions.py (4)</summary>

* `MaxAgentsReached` (227-237)
* `MaxLinkedAccountsReached` (437-445)
* `ProjectNotFound` (73-83)
* `SubscriptionPlanNotFound` (394-404)

</details>
<details>
<summary>backend/aci/common/db/crud/projects.py (1)</summary>

* `get_project` (46-53)

</details>
<details>
<summary>backend/aci/common/db/crud/subscriptions.py (1)</summary>

* `get_subscription_by_org_id` (13-22)

</details>
<details>
<summary>backend/aci/common/db/crud/plans.py (2)</summary>

* `get_by_name` (10-13)
* `get_by_id` (16-19)

</details>
<details>
<summary>backend/aci/common/db/crud/linked_accounts.py (1)</summary>

* `get_linked_accounts` (21-36)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: Format & Lint
* GitHub Check: Compose Tests
* GitHub Check: Format, Lint, and Test

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>backend/aci/common/exceptions.py (1)</summary>

`437-445`: **New exception class looks good.**

The implementation of `MaxLinkedAccountsReached` follows the same pattern as existing quota-related exceptions like `MaxAgentsReached`. It correctly sets a 403 Forbidden status code which is appropriate for quota/limit violations.

</details>
<details>
<summary>backend/aci/server/routes/linked_accounts.py (5)</summary>

`38-38`: **Correctly added quota_manager import.**

The import statement is properly placed among other imports from the same module.

---

`141-144`: **Quota enforcement correctly integrated before account creation.**

The quota enforcement check is properly placed before creating a new linked account, ensuring that the quota is enforced only when actually creating new accounts rather than for existing ones.

---

`220-223`: **Consistent quota enforcement for no-auth accounts.**

The quota enforcement is consistently applied across different account creation endpoints, following the same pattern as in the default credentials endpoint.

---

`313-316`: **Consistent quota enforcement for API key accounts.**

The implementation follows the same pattern as in other endpoints, ensuring consistent quota enforcement across all linked account types.

---

`573-576`: **Consistent quota enforcement for OAuth2 accounts.**

The quota enforcement is properly integrated into the OAuth2 callback flow, ensuring that quota limits are respected across all account creation mechanisms.

</details>
<details>
<summary>backend/aci/server/quota_manager.py (1)</summary>

`14-20`: **Updated imports are comprehensive.**

All necessary exceptions used by the new function have been properly imported.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T17:45:56Z
user: login: dev-aipolabs
body: The unique end users limit refers to the limit of linked account owner ids for all the linked accounts under an org across all the apps instead of one app.

We can add a function in `crud.linked_accounts` called `get_total_number_of_unique_linked_account_ids` that returns the result we want. You can use SQLAlchemy to write a query to get this number directly from the database instead of doing any calculation in your code.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T19:56:25Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>backend/aci/server/quota_manager.py (2)</summary><blockquote>

`101-113`: **Consider a more graceful handling of subscription plan retrieval**

The code has good error handling for subscription and plan lookup, but consider refactoring for more maintainability.

You could refactor the subscription and plan retrieval logic into a helper function to make the code more modular and easier to maintain:

```diff
-    # Get the organization's subscription
-    subscription = crud.subscriptions.get_subscription_by_org_id(db_session, org_id)
-    if not subscription:
-        # If no subscription found, use the free plan
-        plan = crud.plans.get_by_name(db_session, "free")
-        if not plan:
-            raise SubscriptionPlanNotFound("Free plan not found")
-    else:
-        # Get the plan from the subscription
-        plan = crud.plans.get_by_id(db_session, subscription.plan_id)
-        if not plan:
-            raise SubscriptionPlanNotFound(f"Plan {subscription.plan_id} not found")
+    # Get the organization's plan
+    plan = _get_org_plan(db_session, org_id)
```

With a helper function like:

```python
def _get_org_plan(db_session: Session, org_id: UUID):
    """Get organization's plan or default to free plan."""
    subscription = crud.subscriptions.get_subscription_by_org_id(db_session, org_id)
    if not subscription:
        # If no subscription found, use the free plan
        plan = crud.plans.get_by_name(db_session, "free")
        if not plan:
            raise SubscriptionPlanNotFound("Free plan not found")
        return plan
    
    # Get the plan from the subscription
    plan = crud.plans.get_by_id(db_session, subscription.plan_id)
    if not plan:
        raise SubscriptionPlanNotFound(f"Plan {subscription.plan_id} not found")
    return plan
```

---

`117-131`: **Consider pagination for retrieving linked accounts**

The current implementation retrieves all linked accounts for a project and app which could be inefficient for projects with many accounts.

For better performance, especially with large numbers of linked accounts, consider using a count query instead of retrieving all records:

```diff
-    linked_accounts = crud.linked_accounts.get_linked_accounts(
-        db_session, project_id, app_name, None
-    )
-    if len(linked_accounts) >= max_linked_accounts:
+    linked_accounts_count = crud.linked_accounts.count_linked_accounts(
+        db_session, project_id, app_name
+    )
+    if linked_accounts_count >= max_linked_accounts:
```

This would require adding a new `count_linked_accounts` function to the CRUD module that uses `func.count()` instead of retrieving all records.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f1181d34d098946939ee981463c4a1251b4d671d and 201152cc073d65598d44d388564ed5d756f08d45.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `backend/aci/common/exceptions.py` (1 hunks)
* `backend/aci/server/quota_manager.py` (2 hunks)
* `backend/aci/server/routes/linked_accounts.py` (5 hunks)
* `backend/aci/server/tests/routes/linked_acounts/test_accounts_quota.py` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* backend/aci/common/exceptions.py
* backend/aci/server/routes/linked_accounts.py
* backend/aci/server/tests/routes/linked_acounts/test_accounts_quota.py

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>backend/aci/server/quota_manager.py (5)</summary><blockquote>

<details>
<summary>backend/aci/common/exceptions.py (5)</summary>

* `MaxAgentsReached` (227-237)
* `MaxLinkedAccountsReached` (437-445)
* `MaxProjectsReached` (214-224)
* `ProjectNotFound` (73-83)
* `SubscriptionPlanNotFound` (394-404)

</details>
<details>
<summary>backend/aci/common/db/crud/projects.py (1)</summary>

* `get_project` (46-53)

</details>
<details>
<summary>backend/aci/common/db/crud/subscriptions.py (1)</summary>

* `get_subscription_by_org_id` (13-22)

</details>
<details>
<summary>backend/aci/common/db/crud/plans.py (2)</summary>

* `get_by_name` (10-13)
* `get_by_id` (16-19)

</details>
<details>
<summary>backend/aci/common/db/crud/linked_accounts.py (1)</summary>

* `get_linked_accounts` (21-36)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: Compose Tests
* GitHub Check: Format & Lint
* GitHub Check: Format, Lint, and Test

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>backend/aci/server/quota_manager.py (5)</summary>

`14-20`: **LGTM: Appropriate exception imports**

The imports are correctly updated to include all necessary exceptions for the new quota enforcement functionality.

---

`75-90`: **Well-structured function signature and docstring**

The function signature is clear, and the docstring thoroughly documents the function's purpose, parameters, and potential exceptions.

---

`91-99`: **LGTM: Proper project validation**

The code correctly retrieves the project and handles the not-found case with appropriate error logging and exception raising.

---

`73-132`: **Well-implemented quota enforcement function**

Overall, the function follows the established pattern for quota enforcement in this module and is well-implemented with proper error handling, logging, and clear logic flow.


However, consider updating the module docstring (lines 1-7) to mention linked accounts quota enforcement, as it currently only mentions "maximum projects per user, API rate limits, storage quotas."

---

`114-116`: <details>
<summary>✅ Verification successful</summary>

**Consider a safer default for linked_accounts quota**

Using a default of 0 for the linked_accounts quota means users without this feature defined will not be able to create any linked accounts.

Check if this is the intended behavior or if a small positive default would be more appropriate:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if other quota features have non-zero defaults
rg "features.get\(\".*\", [1-9]" --type py
```

Length of output: 42

---



</details>

**linked_accounts quota default aligns with existing behavior**  
Our search didn’t turn up any other `plan.features.get(…, <non-zero>)` calls, which indicates all quota features default to 0 when not defined—intentionally preventing use of features outside a customer’s plan. No code changes are required here unless you decide that users should receive a positive “starter” allowance for linked accounts when the feature isn’t explicitly enabled.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T20:22:26Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>backend/aci/common/db/crud/linked_accounts.py (1)</summary><blockquote>

`171-173`: **Consider adding filtering parameters for quota enforcement**

The function has no parameters for filtering counts by project, app, or organization, which may be needed for proper quota enforcement at different levels.


Consider enhancing the function to support targeted counting:

```diff
-def get_total_number_of_unique_linked_account_ids(db_session: Session) -> int:
-    statement = select(LinkedAccount)
-    return len(db_session.execute(statement).scalars().all())
+def get_total_number_of_linked_accounts(
+    db_session: Session,
+    project_id: UUID | None = None,
+    app_id: UUID | None = None,
+    organization_id: UUID | None = None
+) -> int:
+    from sqlalchemy import func
+    statement = select(func.count()).select_from(LinkedAccount)
+    
+    if project_id:
+        statement = statement.filter(LinkedAccount.project_id == project_id)
+    if app_id:
+        statement = statement.filter(LinkedAccount.app_id == app_id)
+    if organization_id:
+        # Assuming you have a way to filter by organization
+        statement = statement.join(App).filter(App.organization_id == organization_id)
+        
+    return db_session.execute(statement).scalar_one()
```

This would provide flexibility for enforcing quotas at different levels while maintaining performance.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 201152cc073d65598d44d388564ed5d756f08d45 and 0de3681c07211adc1d3ee5ebb2d9178824fc4387.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `backend/aci/common/db/crud/linked_accounts.py` (1 hunks)
* `backend/aci/server/quota_manager.py` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* backend/aci/server/quota_manager.py

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>backend/aci/common/db/crud/linked_accounts.py (1)</summary><blockquote>

<details>
<summary>backend/aci/common/db/sql_models.py (1)</summary>

* `LinkedAccount` (381-438)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: Compose Tests
* GitHub Check: Format, Lint, and Test
* GitHub Check: Format & Lint

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T22:32:28Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 0de3681c07211adc1d3ee5ebb2d9178824fc4387 and ff298719b70d81a1aa472af21bb09acc67988b78.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `backend/aci/common/db/crud/linked_accounts.py` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>backend/aci/common/db/crud/linked_accounts.py (1)</summary><blockquote>

<details>
<summary>backend/aci/common/db/sql_models.py (1)</summary>

* `LinkedAccount` (381-438)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: Format & Lint
* GitHub Check: Compose Tests
* GitHub Check: Format, Lint, and Test

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>backend/aci/common/db/crud/linked_accounts.py (1)</summary>

`4-4`: **Appropriate imports added for SQL functions**

Good addition of the necessary imports `distinct` and `func` from SQLAlchemy to support the new counting functionality.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T12:13:37Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>backend/aci/server/tests/routes/linked_acounts/test_accounts_quota.py (1)</summary><blockquote>

`10-39`: **Fix test comment inconsistency.**

There's a discrepancy in the test comment on line 28. The comment states "Try to create third linked account" but the test is actually attempting to create a second account, and the owner ID is "test_linked_accounts_quota_3" which suggests a missing intermediate step.


```diff
-    # Try to create third linked account (should fail due to quota)
+    # Try to create second linked account (should fail due to quota)
```

Also, consider renaming the owner ID to match the sequence:

```diff
-        linked_account_owner_id="test_linked_accounts_quota_3",
+        linked_account_owner_id="test_linked_accounts_quota_2",
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 2c3fa90ccd1ecd7e6e18a590f9400571cd39efe1 and fb35feecbb2f563a42214fbbe0a4e30535e9fe06.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `backend/aci/common/db/crud/linked_accounts.py` (2 hunks)
* `backend/aci/server/quota_manager.py` (2 hunks)
* `backend/aci/server/tests/conftest.py` (2 hunks)
* `backend/aci/server/tests/routes/linked_acounts/test_accounts_quota.py` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* backend/aci/common/db/crud/linked_accounts.py
* backend/aci/server/quota_manager.py

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>backend/aci/server/tests/routes/linked_acounts/test_accounts_quota.py (4)</summary><blockquote>

<details>
<summary>backend/aci/common/db/sql_models.py (1)</summary>

* `Plan` (468-495)

</details>
<details>
<summary>backend/aci/common/schemas/app_configurations.py (1)</summary>

* `AppConfigurationPublic` (10-29)

</details>
<details>
<summary>backend/aci/common/schemas/linked_accounts.py (1)</summary>

* `LinkedAccountNoAuthCreate` (26-27)

</details>
<details>
<summary>backend/aci/server/tests/conftest.py (4)</summary>

* `test_client` (114-119)
* `dummy_api_key_1` (175-176)
* `dummy_app_configuration_no_auth_mock_app_connector_project_1` (525-541)
* `free_plan` (748-769)

</details>

</blockquote></details>
<details>
<summary>backend/aci/server/tests/conftest.py (3)</summary><blockquote>

<details>
<summary>backend/aci/common/db/sql_models.py (1)</summary>

* `Plan` (468-495)

</details>
<details>
<summary>backend/aci/common/schemas/plans.py (1)</summary>

* `PlanFeatures` (4-10)

</details>
<details>
<summary>backend/aci/common/db/crud/plans.py (1)</summary>

* `create` (31-52)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: Format & Lint
* GitHub Check: Format, Lint, and Test
* GitHub Check: Compose Tests

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>backend/aci/server/tests/conftest.py (1)</summary>

`747-769`: **LGTM: Well-structured fixture for quota testing.**

The `free_plan` fixture provides a suitable test environment for the linked accounts quota feature with appropriate constraints. Setting the linked_accounts limit to 1 creates a clear boundary for testing quota enforcement.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T16:07:12Z
user: login: dev-aipolabs
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T18:45:15Z
user: login: dev-aipolabs
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T18:46:08Z
user: login: alex-aipolabs
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T21:21:40Z
user: login: dev-aipolabs
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T22:03:24Z
user: login: hanyixxx
body: left some comments.
Most of them are suggestive and/or for discussion
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T08:24:57Z
user: login: hanyixxx
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T13:49:59Z

Review comments:
diff_hunk: @@ -166,3 +166,8 @@ def delete_linked_accounts(db_session: Session, project_id: UUID, app_name: str)
         db_session.delete(linked_account)
     db_session.flush()
     return len(linked_accounts_to_delete)
+
+
+def get_total_number_of_unique_linked_account_ids(db_session: Session) -> int:
+    statement = select(LinkedAccount)
+    return len(db_session.execute(statement).scalars().all())
path: backend/aci/common/db/crud/linked_accounts.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Performance issue with loading all records**

The current implementation loads all LinkedAccount records into memory before counting, which is inefficient for large datasets. This approach will become increasingly slow as your database grows.


Optimize by using SQL COUNT directly:

```diff
-def get_total_number_of_unique_linked_account_ids(db_session: Session) -> int:
-    statement = select(LinkedAccount)
-    return len(db_session.execute(statement).scalars().all())
+def get_total_number_of_unique_linked_account_ids(db_session: Session) -> int:
+    from sqlalchemy import func
+    statement = select(func.count()).select_from(LinkedAccount)
+    return db_session.execute(statement).scalar_one()
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
def get_total_number_of_unique_linked_account_ids(db_session: Session) -> int:
    from sqlalchemy import func
    statement = select(func.count()).select_from(LinkedAccount)
    return db_session.execute(statement).scalar_one()
`````

</details>

<!-- suggestion_end -->

---

_⚠️ Potential issue_

**Function name doesn't match implementation**

The function name `get_total_number_of_unique_linked_account_ids` suggests counting distinct linked account IDs, but the implementation counts all linked account records without applying any uniqueness filtering. This could lead to incorrect quota enforcement.


Consider using SQL's COUNT and DISTINCT functions for better performance and accuracy:

```diff
-def get_total_number_of_unique_linked_account_ids(db_session: Session) -> int:
-    statement = select(LinkedAccount)
-    return len(db_session.execute(statement).scalars().all())
+def get_total_number_of_unique_linked_account_ids(db_session: Session) -> int:
+    from sqlalchemy import func, distinct
+    statement = select(func.count(distinct(LinkedAccount.id)))
+    return db_session.execute(statement).scalar_one()
```

Alternatively, if you really need to count all linked accounts (not unique ones), consider renaming the function to `get_total_number_of_linked_accounts` to accurately reflect its purpose.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
def get_total_number_of_unique_linked_account_ids(db_session: Session) -> int:
    from sqlalchemy import func, distinct
    statement = select(func.count(distinct(LinkedAccount.id)))
    return db_session.execute(statement).scalar_one()
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit ff29871
created_at: 2025-05-12T22:32:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -166,3 +166,8 @@ def delete_linked_accounts(db_session: Session, project_id: UUID, app_name: str)
         db_session.delete(linked_account)
     db_session.flush()
     return len(linked_accounts_to_delete)
+
+
+def get_total_number_of_unique_linked_account_ids(db_session: Session) -> int:
+    statement = select(func.count(distinct(LinkedAccount.id)))
+    return db_session.execute(statement).scalar_one()
path: backend/aci/common/db/crud/linked_accounts.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Function implementation doesn't match stated purpose**

This function is named to count "unique linked account IDs", but based on the PR objectives and comments, it should be counting unique linked account *owner* IDs across all linked accounts.

The current implementation counts distinct `LinkedAccount.id` values (primary keys), not distinct `linked_account_owner_id` values (which identify end users).


To fix this issue, modify the function to count distinct owner IDs instead:

```diff
def get_total_number_of_unique_linked_account_ids(db_session: Session) -> int:
-    statement = select(func.count(distinct(LinkedAccount.id)))
+    statement = select(func.count(distinct(LinkedAccount.linked_account_owner_id)))
    return db_session.execute(statement).scalar_one()
```

Alternatively, if counting unique record IDs is the actual intention, consider renaming the function to better reflect its purpose (e.g., `get_total_number_of_linked_accounts`).


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
def get_total_number_of_unique_linked_account_ids(db_session: Session) -> int:
    statement = select(func.count(distinct(LinkedAccount.linked_account_owner_id)))
    return db_session.execute(statement).scalar_one()
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 2c3fa90
created_at: 2025-05-13T12:13:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,39 @@
+from fastapi import status
+from fastapi.testclient import TestClient
+
+from aci.common.db.sql_models import Plan
+from aci.common.schemas.app_configurations import AppConfigurationPublic
+from aci.common.schemas.linked_accounts import LinkedAccountNoAuthCreate
+from aci.server import config
+
+
+def test_linked_accounts_quota(
+    test_client: TestClient,
+    dummy_api_key_1: str,
+    dummy_app_configuration_no_auth_mock_app_connector_project_1: AppConfigurationPublic,
+    free_plan: Plan,
+) -> None:
+    # Create first linked account (should succeed)
+    body1 = LinkedAccountNoAuthCreate(
+        app_name=dummy_app_configuration_no_auth_mock_app_connector_project_1.app_name,
+        linked_account_owner_id="test_linked_accounts_quota_1",
+    )
+    response = test_client.post(
+        f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/no-auth",
+        json=body1.model_dump(mode="json", exclude_none=True),
+        headers={"x-api-key": dummy_api_key_1},
+    )
+    assert response.status_code == status.HTTP_200_OK
+
+    # Try to create third linked account (should fail due to quota)
+    body2 = LinkedAccountNoAuthCreate(
+        app_name=dummy_app_configuration_no_auth_mock_app_connector_project_1.app_name,
+        linked_account_owner_id="test_linked_accounts_quota_3",
+    )
+    response = test_client.post(
+        f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/no-auth",
+        json=body2.model_dump(mode="json", exclude_none=True),
+        headers={"x-api-key": dummy_api_key_1},
+    )
+    assert response.status_code == status.HTTP_403_FORBIDDEN
+    assert "Max linked accounts reached" in str(response.json()["error"])
path: backend/aci/server/tests/routes/linked_acounts/test_accounts_quota.py
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify quota behavior with organization context.**

The PR objective mentions that the quota is for "the total number of unique linked account owner IDs across all linked accounts within an organization." Consider enhancing this test to verify that the quota is enforced at the organization level rather than just at the project level.


Try creating the following verification script to check if the implementation correctly honors the organization-level constraint:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if linked_accounts.py implements organization-level quota

# Search for organization ID references in the quota enforcement code
rg -A 10 "enforce_linked_accounts_creation_quota|get_total_number_of_unique_linked_account_owner_ids" --type py
```

Length of output: 6538

---



</details>

**Extend test to cover organization-level quota enforcement**

The current test only exercises quota within a single project, but the implementation counts linked accounts across all projects in an organization. Please update `backend/aci/server/tests/routes/linked_acounts/test_accounts_quota.py` to verify that:

- Accounts created in a second project under the *same* organization contribute toward the same quota.
- Once the total unique owner count across both projects reaches the plan limit, further creations (in *either* project) are rejected with HTTP 403.

For example, after creating one account in Project A and one in Project B (same org), the next creation—regardless of project—should hit the quota:

```diff
@@ def test_linked_accounts_quota(
     # Create first linked account in Project A (should succeed)
@@
     assert response.status_code == status.HTTP_200_OK

+    # Create second linked account in a different project (same org) — still within quota
+    body_project_b = LinkedAccountNoAuthCreate(
+        app_name=dummy_app_configuration_no_auth_mock_app_connector_project_2.app_name,
+        linked_account_owner_id="test_linked_accounts_quota_b1",
+    )
+    response = test_client.post(
+        f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/no-auth",
+        json=body_project_b.model_dump(mode="json", exclude_none=True),
+        headers={"x-api-key": dummy_api_key_1},
+    )
+    assert response.status_code == status.HTTP_200_OK
+
     # Try to create third linked account (should fail due to quota)
@@
     assert response.status_code == status.HTTP_403_FORBIDDEN
     assert "Max linked accounts reached" in str(response.json()["error"])
```

• Add a fixture for a second `AppConfigurationPublic` in the same org (e.g. `dummy_app_configuration_no_auth_mock_app_connector_project_2`).  
• Confirm that `quota_manager.enforce_linked_accounts_creation_quota` indeed uses `org_id` via `get_total_number_of_unique_linked_account_owner_ids`.  



> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In backend/aci/server/tests/routes/linked_acounts/test_accounts_quota.py lines 1
to 39, the test currently verifies linked account quota only within a single
project, but the quota is enforced across all projects in the same organization.
To fix this, extend the test by adding a fixture for a second
AppConfigurationPublic instance representing another project in the same
organization. Then create linked accounts in both projects and assert that the
total unique linked account owner IDs across these projects count toward the
quota. Finally, verify that once the quota is reached, further linked account
creation attempts in either project return HTTP 403 Forbidden with the
appropriate error message.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits 1e0217c to f36001b
created_at: 2025-05-15T16:07:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,39 @@
+from fastapi import status
+from fastapi.testclient import TestClient
+
+from aci.common.db.sql_models import Plan
+from aci.common.schemas.app_configurations import AppConfigurationPublic
+from aci.common.schemas.linked_accounts import LinkedAccountNoAuthCreate
+from aci.server import config
+
+
+def test_linked_accounts_quota(
+    test_client: TestClient,
+    dummy_api_key_1: str,
+    dummy_app_configuration_no_auth_mock_app_connector_project_1: AppConfigurationPublic,
+    free_plan: Plan,
+) -> None:
+    # Create first linked account (should succeed)
+    body1 = LinkedAccountNoAuthCreate(
+        app_name=dummy_app_configuration_no_auth_mock_app_connector_project_1.app_name,
+        linked_account_owner_id="test_linked_accounts_quota_1",
+    )
+    response = test_client.post(
+        f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/no-auth",
+        json=body1.model_dump(mode="json", exclude_none=True),
+        headers={"x-api-key": dummy_api_key_1},
+    )
+    assert response.status_code == status.HTTP_200_OK
+
+    # Try to create third linked account (should fail due to quota)
+    body2 = LinkedAccountNoAuthCreate(
+        app_name=dummy_app_configuration_no_auth_mock_app_connector_project_1.app_name,
+        linked_account_owner_id="test_linked_accounts_quota_3",
+    )
+    response = test_client.post(
+        f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/no-auth",
+        json=body2.model_dump(mode="json", exclude_none=True),
+        headers={"x-api-key": dummy_api_key_1},
+    )
path: backend/aci/server/tests/routes/linked_acounts/test_accounts_quota.py
user: login: dev-aipolabs
body: This part of the test is essentially relying on the magic number of linked account quota we set in the fixture, which can be confusing to people reading the code as they may not know the quota is set to 1.
created_at: 2025-05-16T18:45:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,39 @@
+from fastapi import status
+from fastapi.testclient import TestClient
+
+from aci.common.db.sql_models import Plan
+from aci.common.schemas.app_configurations import AppConfigurationPublic
+from aci.common.schemas.linked_accounts import LinkedAccountNoAuthCreate
+from aci.server import config
+
+
+def test_linked_accounts_quota(
+    test_client: TestClient,
+    dummy_api_key_1: str,
+    dummy_app_configuration_no_auth_mock_app_connector_project_1: AppConfigurationPublic,
+    free_plan: Plan,
+) -> None:
+    # Create first linked account (should succeed)
+    body1 = LinkedAccountNoAuthCreate(
+        app_name=dummy_app_configuration_no_auth_mock_app_connector_project_1.app_name,
+        linked_account_owner_id="test_linked_accounts_quota_1",
+    )
+    response = test_client.post(
+        f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/no-auth",
+        json=body1.model_dump(mode="json", exclude_none=True),
+        headers={"x-api-key": dummy_api_key_1},
+    )
+    assert response.status_code == status.HTTP_200_OK
+
+    # Try to create third linked account (should fail due to quota)
+    body2 = LinkedAccountNoAuthCreate(
+        app_name=dummy_app_configuration_no_auth_mock_app_connector_project_1.app_name,
+        linked_account_owner_id="test_linked_accounts_quota_3",
+    )
+    response = test_client.post(
+        f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/no-auth",
+        json=body2.model_dump(mode="json", exclude_none=True),
+        headers={"x-api-key": dummy_api_key_1},
+    )
path: backend/aci/server/tests/routes/linked_acounts/test_accounts_quota.py
user: login: dev-aipolabs
body: We should avoid using or relying on magic numbers.
created_at: 2025-05-16T18:46:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,39 @@
+from fastapi import status
+from fastapi.testclient import TestClient
+
+from aci.common.db.sql_models import Plan
+from aci.common.schemas.app_configurations import AppConfigurationPublic
+from aci.common.schemas.linked_accounts import LinkedAccountNoAuthCreate
+from aci.server import config
+
+
+def test_linked_accounts_quota(
+    test_client: TestClient,
+    dummy_api_key_1: str,
+    dummy_app_configuration_no_auth_mock_app_connector_project_1: AppConfigurationPublic,
+    free_plan: Plan,
+) -> None:
+    # Create first linked account (should succeed)
+    body1 = LinkedAccountNoAuthCreate(
+        app_name=dummy_app_configuration_no_auth_mock_app_connector_project_1.app_name,
+        linked_account_owner_id="test_linked_accounts_quota_1",
+    )
+    response = test_client.post(
+        f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/no-auth",
+        json=body1.model_dump(mode="json", exclude_none=True),
+        headers={"x-api-key": dummy_api_key_1},
+    )
+    assert response.status_code == status.HTTP_200_OK
+
+    # Try to create third linked account (should fail due to quota)
+    body2 = LinkedAccountNoAuthCreate(
+        app_name=dummy_app_configuration_no_auth_mock_app_connector_project_1.app_name,
+        linked_account_owner_id="test_linked_accounts_quota_3",
+    )
+    response = test_client.post(
+        f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/no-auth",
+        json=body2.model_dump(mode="json", exclude_none=True),
+        headers={"x-api-key": dummy_api_key_1},
+    )
path: backend/aci/server/tests/routes/linked_acounts/test_accounts_quota.py
user: login: alex-aipolabs
body: I'm not really sure how to approach this, if we have the plan as a fixture we need to specify the PlanFeatures we are using which includes the linked account number, agent credentials etc. We can potentially use a constant such as PLAN_FEATURES and then log the linked account number in the test as context. The second option is to commit the plan from the test itself which can include duplication.
created_at: 2025-05-16T21:21:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,39 @@
+from fastapi import status
+from fastapi.testclient import TestClient
+
+from aci.common.db.sql_models import Plan
+from aci.common.schemas.app_configurations import AppConfigurationPublic
+from aci.common.schemas.linked_accounts import LinkedAccountNoAuthCreate
+from aci.server import config
+
+
+def test_linked_accounts_quota(
+    test_client: TestClient,
+    dummy_api_key_1: str,
+    dummy_app_configuration_no_auth_mock_app_connector_project_1: AppConfigurationPublic,
+    free_plan: Plan,
+) -> None:
+    # Create first linked account (should succeed)
+    body1 = LinkedAccountNoAuthCreate(
+        app_name=dummy_app_configuration_no_auth_mock_app_connector_project_1.app_name,
+        linked_account_owner_id="test_linked_accounts_quota_1",
+    )
+    response = test_client.post(
+        f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/no-auth",
+        json=body1.model_dump(mode="json", exclude_none=True),
+        headers={"x-api-key": dummy_api_key_1},
+    )
+    assert response.status_code == status.HTTP_200_OK
+
+    # Try to create third linked account (should fail due to quota)
+    body2 = LinkedAccountNoAuthCreate(
+        app_name=dummy_app_configuration_no_auth_mock_app_connector_project_1.app_name,
+        linked_account_owner_id="test_linked_accounts_quota_3",
+    )
+    response = test_client.post(
+        f"{config.ROUTER_PREFIX_LINKED_ACCOUNTS}/no-auth",
+        json=body2.model_dump(mode="json", exclude_none=True),
+        headers={"x-api-key": dummy_api_key_1},
+    )
path: backend/aci/server/tests/routes/linked_acounts/test_accounts_quota.py
user: login: dev-aipolabs
body: We can use the `free_plan` fixture directly to get the quota value.
created_at: 2025-05-16T22:03:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,3 +69,60 @@ def enforce_agent_creation_quota(db_session: Session, project_id: UUID) -> None:
             },
         )
         raise MaxAgentsReached()
+
+
+def enforce_linked_accounts_creation_quota(
+    db_session: Session, org_id: UUID, linked_account_owner_id: str
+) -> None:
+    """
+    Check and enforce that the organization hasn't exceeded its unique linked account owner ids
+    quota, which is determined by the organization's current subscription plan.
+
+    Args:
+        db_session: Database session
+        org_id: ID of the organization to check
+        linked_account_owner_id: ID of the linked account owner to check
path: backend/aci/server/quota_manager.py
user: login: hanyixxx
body: to make the description more specific, maybe something like "The linked_account_owner_id of the current linked account creation request" ?
"ID of the linked account owner to check" makes it sounds more like we can checking this specific owner id
created_at: 2025-05-19T07:49:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -561,6 +581,11 @@ async def linked_accounts_oauth2_callback(
             db_session, linked_account, security_credentials
         )
     else:
+        # Enforce linked accounts quota before creating new account
+        quota_manager.enforce_linked_accounts_creation_quota(
+            db_session, state.project_id, state.linked_account_owner_id
path: backend/aci/server/routes/linked_accounts.py
user: login: hanyixxx
body: should pass in the org_id instead of project_id?
created_at: 2025-05-19T07:55:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,3 +69,60 @@ def enforce_agent_creation_quota(db_session: Session, project_id: UUID) -> None:
             },
         )
         raise MaxAgentsReached()
+
+
+def enforce_linked_accounts_creation_quota(
+    db_session: Session, org_id: UUID, linked_account_owner_id: str
path: backend/aci/server/quota_manager.py
user: login: hanyixxx
body: just a thought but worth discussion: should we limit the linked account owner id per project instead of per org? (And we already have project creation quota so user can't bypass this by using multiple projects. Even if they can they probably won't due to the management overhead)
the reasoning being:
- `project` is the resource and logical container in our platform
- `linked accounts` are resources under project and won't be shared by other projects within the org
- In enterprise settings, they probably wouldn't like that Dev A's project will affect Dev B's project during runtime (Dev A's project used up all linked account owner id quota.) 

However, for the 3rd point, on the flip side this is probably fine too, and we already have this problem because api key quota is per org. (Which is normal, e.g., openai has similiar setup)

@ObnoxiousProxy what's your thought?
created_at: 2025-05-19T08:02:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -166,3 +166,20 @@ def delete_linked_accounts(db_session: Session, project_id: UUID, app_name: str)
         db_session.delete(linked_account)
     db_session.flush()
     return len(linked_accounts_to_delete)
+
+
+def get_total_number_of_unique_linked_account_owner_ids(db_session: Session, org_id: UUID) -> int:
path: backend/aci/common/db/crud/linked_accounts.py
user: login: hanyixxx
body: technically, both sql operations here need locking to avoid a race condition. (if multiple requests hit at the same time, they could all see a count below the quota, and proceed to insert, exceeding the quota.)
Either a mutex on application level or row-level locking here.
However, this might not be super necessary as it's pretty low stake. (so maybe just leave a comment at where this is called, specifying the potential race condition?)
created_at: 2025-05-19T08:22:09Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-12T17:43:02Z
body: [vc]: #WuOtpQSrIZyqg8VzqpoQyDPIkbLG1hXiV0ZEr9BEwdA=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJhY2ktZGV2LXBvcnRhbCIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9vYm5veGlvdXNwcm94eXMtcHJvamVjdHMvYWNpLWRldi1wb3J0YWwvRzJSaFhUdENHWGJaWFRqTEpkN3ZNaGpDUFJGZSIsInByZXZpZXdVcmwiOiJhY2ktZGV2LXBvcnRhbC1naXQtZmVhdC1saW5rZWQtOTE2MDQzLW9ibm94aW91c3Byb3h5cy1wcm9qZWN0cy52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImFjaS1kZXYtcG9ydGFsLWdpdC1mZWF0LWxpbmtlZC05MTYwNDMtb2Jub3hpb3VzcHJveHlzLXByb2plY3RzLnZlcmNlbC5hcHAifSwicm9vdERpcmVjdG9yeSI6ImZyb250ZW5kIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **aci-dev-portal** | ✅ Ready ([Inspect](https://vercel.com/obnoxiousproxys-projects/aci-dev-portal/G2RhXTtCGXbZXTjLJd7vMhjCPRFe)) | [Visit Preview](https://aci-dev-portal-git-feat-linked-916043-obnoxiousproxys-projects.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/aci-dev-portal-git-feat-linked-916043-obnoxiousproxys-projects.vercel.app?via=pr-comment-feedback-link) | May 19, 2025 1:48pm |


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-12T17:43:04Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

A new exception for handling maximum unique linked account owner IDs has been introduced, along with a quota enforcement function that restricts the number of unique linked account owner IDs per organization based on its subscription plan. Quota checks are integrated into linked account creation routes for various credential types. Tests verify quota enforcement across single and multiple projects. Utility functions were added to query linked account owner ID counts and existence within an organization. A new pytest fixture provides a free subscription plan for testing.

## Changes

| File(s)                                                                                      | Change Summary                                                                                                              |
|----------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
| backend/aci/common/exceptions.py                                                             | Added `MaxUniqueLinkedAccountOwnerIdsReached` exception for quota violations on unique linked account owner IDs.            |
| backend/aci/common/db/crud/linked_accounts.py                                                | Added `get_total_number_of_unique_linked_account_owner_ids` and `linked_account_owner_id_exists_in_org` functions.          |
| backend/aci/server/quota_manager.py                                                          | Added `enforce_linked_accounts_creation_quota` function to enforce unique linked account owner ID quotas per organization.  |
| backend/aci/server/routes/linked_accounts.py                                                 | Added quota enforcement calls before linked account creation for all credential types; updated imports.                     |
| backend/aci/server/tests/routes/linked_acounts/test_accounts_quota.py                        | Added tests verifying linked account owner ID quota enforcement for single and multiple projects within an organization.    |
| backend/aci/server/tests/conftest.py                                                         | Added pytest fixture `free_plan` defining a free subscription plan with linked account limits for use in tests.             |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant API
    participant QuotaManager
    participant DB

    User->>API: Request to create linked account with owner ID
    API->>DB: Check if linked account owner ID exists in org
    alt Owner ID exists
        API->>DB: Proceed with creation (no quota check needed)
        API-->>User: Respond 200 OK or relevant success
    else Owner ID does not exist
        API->>QuotaManager: enforce_linked_accounts_creation_quota(org_id, owner_id)
        QuotaManager->>DB: Retrieve subscription plan and current unique owner ID count
        alt Quota exceeded
            QuotaManager-->>API: Raise MaxUniqueLinkedAccountOwnerIdsReached (403)
            API-->>User: Respond 403 Forbidden
        else Quota not exceeded
            API->>DB: Create linked account
            API-->>User: Respond 200 OK
        end
    end
```

## Poem

> 🐇 In fields of code where linked accounts grow,  
> A limit now guards how many can show.  
> Unique owners counted, a quota in place,  
> To keep order and balance in this digital space.  
> Raise the flag if you try to exceed,  
> “Max unique owners!” the system will heed.  
> Hop along wisely, and all will be right! 🌿✨

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!NOTE]
> <details>
> <summary>⚡️ AI Code Reviews for VS Code, Cursor, Windsurf</summary>
> 
> CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
> Learn more [here](http://coderabbit.ai/ide).
> 
> </details>

---

> [!NOTE]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between e6366052504c76187f651df3fc3be9d5c3c34c7c and ed132378c08cd1a02b453ebe33cb1840347cdb05.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `backend/aci/common/db/crud/linked_accounts.py` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* backend/aci/common/db/crud/linked_accounts.py

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: Format & Lint
* GitHub Check: Format, Lint, and Test
* GitHub Check: Compose Tests

</details>

</details>
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=aipotheosis-labs/aci&utm_content=354):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: alex-aipolabs
created_at: 2025-05-13T11:20:19Z
body: > The unique end users limit refers to the limit of linked account owner ids for all the linked accounts under an org across all the apps instead of one app.
> 
> We can add a function in `crud.linked_accounts` called `get_total_number_of_unique_linked_account_ids` that returns the result we want. You can use SQLAlchemy to write a query to get this number directly from the database instead of doing any calculation in your code.

Cool fixed this
author_association: CONTRIBUTOR
user: login: dev-aipolabs
created_at: 2025-05-16T17:30:42Z
body: These two files are empty. Are you trying to add more tests?

<img width="1739" alt="Screenshot 2025-05-16 at 6 28 58 PM" src="https://github.com/user-attachments/assets/c147d4ca-719b-4b26-aaa9-1403ed3b39b8" />

author_association: CONTRIBUTOR
user: login: sentry[bot]
created_at: 2025-05-23T08:01:13Z
body: ## Suspect Issues
This pull request was deployed and Sentry observed the following issues:

- ‼️ **MultipleResultsFound: Multiple rows were found when one or none was required** `/v1/linked-accounts/oauth2` [View Issue](https://aipotheosis-labs.sentry.io/issues/6625752312/?referrer=github-pr-bot)
- ‼️ **MultipleResultsFound: Multiple rows were found when one or none was required** `/v1/linked-accounts/api-key` [View Issue](https://aipotheosis-labs.sentry.io/issues/6626857236/?referrer=github-pr-bot)

<sub>Did you find this useful? React with a 👍 or 👎</sub>
author_association: NONE