Skip to content

(feat): add endpoints for admin and merchants dashboards#508

Open
narsimhaReddyJuspay wants to merge 1 commit intojuspay:releasefrom
narsimhaReddyJuspay:add-user-crud-endpoints
Open

(feat): add endpoints for admin and merchants dashboards#508
narsimhaReddyJuspay wants to merge 1 commit intojuspay:releasefrom
narsimhaReddyJuspay:add-user-crud-endpoints

Conversation

@narsimhaReddyJuspay
Copy link
Contributor

@narsimhaReddyJuspay narsimhaReddyJuspay commented Jan 23, 2026

Summary by CodeRabbit

  • New Features

    • Full merchant-entity and user-account management APIs: CRUD, RBAC-enforced access (admin/reseller/owner scopes), pagination, filtering, and sorting; legacy merchant surface preserved for compatibility.
  • Documentation

    • Added comprehensive merchant/user management guide covering architecture, RBAC, migrations, and deployment.
  • Chores

    • Role renamed (SHOP → USER) and migration added to separate merchants (business entities) from login accounts.

@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Separates merchant business entities from user login accounts: adds merchant_entities and user_accounts routers plus legacy users router, new Pydantic schemas, DB CRUD modules, migration 014 (merchants table + owner_id), RBAC updates (SHOP→USER), and handlers enforcing scoped permissions and audit logging.

Changes

Cohort / File(s) Summary
Top-level router changes
app/api/routers/breeze_buddy/__init__.py
Replace merchants_router with merchant_entities_router (mounted at /entity, tag merchant-entities), user_accounts_router (root, tag user-accounts), and users_router (root, tag merchants-legacy).
Routers (API layer)
app/api/routers/breeze_buddy/users/__init__.py, app/api/routers/breeze_buddy/merchant_entities/__init__.py, app/api/routers/breeze_buddy/user_accounts/__init__.py
Add three APIRouter modules exposing admin-only legacy merchant endpoints, merchant-entity CRUD (admin/reseller), and RBAC-scoped user-account CRUD.
Router handlers
app/api/routers/breeze_buddy/users/handlers.py, app/api/routers/breeze_buddy/merchant_entities/handlers.py, app/api/routers/breeze_buddy/user_accounts/handlers.py
Implement create/list/get/update/delete handlers with RBAC checks, ownership scoping, validation, audit logging, and HTTP error mapping.
Database queries — users & merchant_entities
app/database/queries/breeze_buddy/users.py, app/database/queries/breeze_buddy/merchant_entities.py
Add comprehensive CRUD DB functions for merchants/entities and users: paginated listings, create/get/update/delete, JSONB parsing, response builders, existence checks, password hashing integration, and helper utilities.
DB migration
app/database/migrations/014_create_merchants_table.sql
Add merchants table (merchant_id PK, name, description, shop_identifiers JSONB, is_active, owner_id, timestamps), indexes; add owner_id to users; migrate role values (shopuser) and update role check constraint.
Schemas
app/schemas/breeze_buddy/users.py, app/schemas/breeze_buddy/merchant_entities.py, app/schemas/breeze_buddy/auth.py, app/schemas/breeze_buddy/__init__.py, app/schemas/__init__.py
Introduce Merchant*/MerchantEntity*/User* schemas and DeleteResponse; add owner_id to User models; rename UserRole SHOP→USER; export/alias user-account schema names and update package exports.
Authorization tweak
app/core/security/authorization.py
Adjust is_shop() to treat UserRole.USER as the shop-equivalent role (reflects role rename).
Docs & guide
MERCHANT_USER_MANAGEMENT_GUIDE.md
Add detailed design/migration/operation guide covering separation of merchants vs users, RBAC model, migration steps, endpoint contracts, testing and troubleshooting.
Manifests & packaging
manifest_file, requirements.txt, pyproject.toml
Manifest/package files referenced/updated within the diff.
Top-level schema exports
app/schemas/__init__.py, app/schemas/breeze_buddy/__init__.py
Expose new schemas and legacy aliases; include DeleteResponse and MerchantEntity* / UserAccount* public exports.

Sequence Diagram(s)

sequenceDiagram
    participant Admin as Admin/Reseller
    participant API as API Router
    participant AuthZ as RBAC
    participant Handler as Handler Layer
    participant DB as Database

    Admin->>API: POST /entity/merchant (MerchantEntityCreate)
    API->>AuthZ: get_current_user_with_rbac
    AuthZ-->>API: UserInfo (role, owner_id, scope)
    API->>Handler: create_merchant_entity_handler(merchant_data, current_user)
    Handler->>DB: check_merchant_id_exists(merchant_id)
    DB-->>Handler: exists? (false)
    Handler->>DB: INSERT INTO merchants (...)
    DB-->>Handler: created row
    Handler-->>API: MerchantEntityResponse (201)
    API-->>Admin: 201 MerchantEntityResponse
Loading
sequenceDiagram
    participant Admin as Admin
    participant API as API Router
    participant AuthZ as RBAC
    participant Handler as Handler Layer
    participant DB as Database

    Admin->>API: POST /user (UserCreate)
    API->>AuthZ: get_current_user_with_rbac
    AuthZ-->>API: UserInfo (role, owner_id)
    API->>Handler: create_user_handler(user_data, current_user)
    Handler->>Handler: _check_create_access
    Handler->>DB: create_user(username, hashed_password, role, owner_id, ...)
    DB-->>Handler: User row
    Handler-->>API: UserResponse (201)
    API-->>Admin: 201 UserResponse
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇 I hopped through code to split the lairs,
Merchants in burrows, users with their keys and cares.
Owner IDs etched like names on a tree,
SHOP became USER — neat as can be,
A rabbit's cheer for schema harmony!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '(feat): add endpoints for admin and merchants dashboards' is partially related to the changeset. While the PR does add endpoints, it encompasses significantly more than just dashboard endpoints, including CRUD operations, database migrations, schemas, handlers, RBAC controls, and comprehensive user/merchant management infrastructure.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@app/api/routers/breeze_buddy/users/handlers.py`:
- Around line 399-445: Add a guard in update_user_handler to prevent merchants
from updating accounts that are not SHOP: after obtaining target_user via
check_user_ownership(current_user, user_id), if current_user.role ==
UserRole.MERCHANT and target_user.role != UserRole.SHOP raise HTTPException 403
(e.g., "Merchants can only modify SHOP accounts"); keep the existing
merchant-specific checks (disallow changing role and restrict shop_identifiers)
but reference target_user where relevant so target_user is meaningfully used.

In `@app/database/queries/breeze_buddy/user_management.py`:
- Around line 77-98: Both check_username_exists and check_merchant_id_exists
currently catch all exceptions and return False, which treats DB failures as
“does not exist”; change them to log the error and re-raise the exception (or
raise a specific custom DB error) instead of returning False so callers can fail
safely. Specifically, in check_username_exists and check_merchant_id_exists (and
inside the async for get_db_connection() blocks) remove the swallowing return
False in the except clause, call logger.error(...) as now, then raise the caught
exception (or wrap it in a custom exception type) so callers receive the error
rather than a false existence result.
- Around line 243-272: The TOCTOU bug in delete_merchant_with_users can delete
dependent users before confirming the merchant exists; inside the same
transaction obtained via get_db_connection(), first SELECT the merchant row
using a FOR UPDATE lock (e.g., SELECT id FROM users WHERE role='merchant' AND
merchant_ids::jsonb ? $1 FOR UPDATE) to confirm existence and lock it, and only
if that returns a row proceed to run the DELETE for non-merchant users
(deleted_users_query) and then delete the merchant (delete_merchant_query); keep
the entire sequence inside the async with conn.transaction() block so the check
and deletes are atomic and roll back on failure.

In `@app/schemas/breeze_buddy/users.py`:
- Around line 11-19: The MerchantCreate model currently defaults
shop_identifiers to an empty list which gives new merchants no shop access;
confirm the intended behavior and either (A) change the default on the
shop_identifiers Field in MerchantCreate to represent “all shops” (e.g., default
to ["*"]) so new merchants inherit universal access, or (B) keep the empty list
but update any documentation and docstrings to explicitly state that merchants
start with no shop access unless shop_identifiers are provided; locate and
modify the Field declaration named shop_identifiers in the MerchantCreate class
accordingly and update related docs/comments or tests that assume the opposite
behavior.

In `@docs/USER_MANAGEMENT_API.md`:
- Around line 9-13: The fenced code blocks in USER_MANAGEMENT_API.md (the role
hierarchy block and the app/ file-structure block) are missing language
identifiers which triggers markdownlint MD040; update each triple-backtick fence
to include a language identifier such as "text" (e.g., ```text) for both the
block showing "Admin (Full Access) ├── Merchant..." and the block showing the
app/ directory tree so linting passes.
- Around line 57-61: The markdown tables (e.g., the parameter table containing
the `page` and `limit` rows) are missing blank lines before and after them which
triggers MD058; surround each table block with a blank line above and below to
satisfy markdownlint. Locate the tables that list parameters (those containing
`page`, `limit`, and similar parameter rows found throughout the doc) and insert
a single empty line immediately before the opening pipe row and a single empty
line immediately after the closing pipe row for each occurrence (including the
other listed table blocks).
🧹 Nitpick comments (1)
app/schemas/__init__.py (1)

61-71: Export MerchantDeleteResponse alongside other user-management schemas.

DeleteResponse is re-exported, but MerchantDeleteResponse isn’t, even though the delete-merchant endpoint uses it. Re-exporting keeps the public schema surface consistent.

♻️ Proposed update
 from app.schemas.breeze_buddy.users import (
     DeleteResponse,
     MerchantCreate,
+    MerchantDeleteResponse,
     MerchantListResponse,
     MerchantResponse,
     MerchantUpdate,
     UserCreateRequest,
     UserListResponse,
     UserResponse,
     UserUpdateRequest,
 )
@@
     # User Management
     "DeleteResponse",
+    "MerchantDeleteResponse",
     "MerchantCreate",
     "MerchantListResponse",
     "MerchantResponse",
     "MerchantUpdate",
     "UserCreateRequest",
     "UserListResponse",
     "UserResponse",
     "UserUpdateRequest",
 ]

Also applies to: 117-126

@narsimhaReddyJuspay narsimhaReddyJuspay force-pushed the add-user-crud-endpoints branch 5 times, most recently from 4931be9 to 85bf78c Compare January 27, 2026 08:23
@narsimhaReddyJuspay
Copy link
Contributor Author

@coderabbitai i have changed a lot of things please review agian

@coderabbitai
Copy link

coderabbitai bot commented Jan 27, 2026

@narsimhaReddyJuspay, I'll perform a comprehensive review of all the changes in this PR, including the new endpoints for admin and merchant dashboards, database queries, schemas, and documentation.

✅ Actions performed

Full review triggered.

@narsimhaReddyJuspay
Copy link
Contributor Author

@coderabbitai please review code

@coderabbitai
Copy link

coderabbitai bot commented Feb 3, 2026

@narsimhaReddyJuspay, I'll review all the code changes in this PR, including the new merchant management endpoints, database queries, schemas, and router configurations.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@narsimhaReddyJuspay narsimhaReddyJuspay force-pushed the add-user-crud-endpoints branch 2 times, most recently from 8d1ebf8 to 509dd68 Compare February 3, 2026 13:27
@narsimhaReddyJuspay
Copy link
Contributor Author

narsimhaReddyJuspay commented Feb 3, 2026

@coderabbitai review again carefully

@coderabbitai
Copy link

coderabbitai bot commented Feb 3, 2026

@narsimhaReddyJuspay, I'll conduct a thorough review of all the changes. Based on the current implementation, here are some key observations:

Immediate Observations

1. Naming Inconsistency
The users_router is being used to handle merchant endpoints (tagged as "merchants"). This creates confusion in the codebase:

# In app/api/routers/breeze_buddy/__init__.py
from app.api.routers.breeze_buddy.users import router as users_router
router.include_router(users_router, prefix="", tags=["merchants"])

Consider renaming to merchant_users_router or keeping the module name aligned with its purpose.

2. Security Implementation

  • ✅ Passwords are being hashed via hash_password
  • ✅ Admin-only access enforced with require_admin helper
  • ✅ RBAC implemented with get_current_user_with_rbac

3. Potential Areas for Review

  • Validation: Check username/merchant_id uniqueness handling in concurrent requests
  • Error Messages: Verify sensitive information isn't leaked in error responses
  • Database Queries: Review SQL injection prevention and connection handling
  • Input Validation: Ensure all fields have appropriate constraints
  • Audit Logging: Verify all admin actions are properly logged
  • Transaction Handling: Check if operations that span multiple queries use transactions

I'll now trigger a comprehensive full review to analyze all aspects in detail.

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link

coderabbitai bot commented Feb 3, 2026

@narsimhaReddyJuspay, I'll conduct a thorough review of all the changes. From the code changes, I can see this PR introduces a comprehensive merchant management system with admin-only access controls. Here are some areas I'll focus on during the careful review:

Key Changes to Review:

  1. Access Control: Admin-only RBAC enforcement via require_admin() helper
  2. Merchant CRUD Operations: Complete create, read, update, delete functionality
  3. Database Operations: New queries with pagination, validation helpers, and error handling
  4. API Schemas: Request/response models for merchant management
  5. Router Configuration: Migration from merchants_router to users_router

Areas of Focus:

  • Security: Password hashing, permission checks, input validation
  • Error handling: Database errors, conflict detection (username/merchant_id uniqueness)
  • Data integrity: JSON field parsing, timestamp handling, optional field updates
  • API design: Pagination parameters, response models, error responses
  • Code organization: Handler separation, query abstraction

I'll now trigger a comprehensive review of all these changes.

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@app/database/queries/breeze_buddy/users.py`:
- Around line 105-127: The pre-insert uniqueness checks (check_username_exists,
check_merchant_id_exists) are racy; to handle conflicts you should catch unique
constraint violations around the actual insert: wrap the call to create_merchant
inside create_merchant_handler in a try/except that catches
asyncpg.exceptions.UniqueViolationError (or IntegrityError) and return an HTTP
409 CONFLICT response with a clear message; alternatively modify the insertion
in create_merchant to use an INSERT ... ON CONFLICT DO NOTHING/UPDATE and return
a handled result. Ensure you import asyncpg.exceptions.UniqueViolationError
where create_merchant_handler is defined and avoid letting the exception
propagate.
🧹 Nitpick comments (3)
app/schemas/breeze_buddy/users.py (1)

40-40: Consider using EmailStr for email validation.

The email field accepts any string. If email format validation is desired, Pydantic's EmailStr type provides built-in validation.

♻️ Optional: Add email validation
 from datetime import datetime
 from typing import List, Optional
 
-from pydantic import BaseModel, Field
+from pydantic import BaseModel, EmailStr, Field
 
 from app.schemas.breeze_buddy.auth import UserRole

Then update the field definitions:

-    email: Optional[str] = Field(None, description="Email address of the merchant")
+    email: Optional[EmailStr] = Field(None, description="Email address of the merchant")

Also applies to: 59-59

app/database/queries/breeze_buddy/users.py (1)

209-218: Minor: Use unpacking syntax for list concatenation.

♻️ Cleaner list construction
     # Ensure primary merchant_id is in the list
     elif merchant_id not in merchant_ids:
-        merchant_ids = [merchant_id] + merchant_ids
+        merchant_ids = [merchant_id, *merchant_ids]
app/api/routers/breeze_buddy/__init__.py (1)

44-49: Consider renaming for clarity.

The users_router handles merchant endpoints and is tagged as "merchants". While functionally correct, this naming could cause confusion. Consider renaming to merchant_users_router or merchants_router for alignment.

This is a minor naming inconsistency and doesn't affect functionality.

♻️ Optional: Rename for clarity

In app/api/routers/breeze_buddy/users/__init__.py, the module could be renamed to merchants or the router aliased:

-from app.api.routers.breeze_buddy.users import router as users_router
+from app.api.routers.breeze_buddy.users import router as merchants_router
 
 ...
 
 # Users/Merchants (merchant management - RBAC enabled)
-router.include_router(users_router, prefix="", tags=["merchants"])
+router.include_router(merchants_router, prefix="", tags=["merchants"])

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@app/api/routers/breeze_buddy/users/handlers.py`:
- Around line 97-111: The call to create_merchant should be wrapped in a
try/except that catches asyncpg.exceptions.UniqueViolationError so concurrent
uniqueness constraint failures return a 409 instead of a 500; update the block
around create_merchant(...) in users/handlers.py to catch
asyncpg.exceptions.UniqueViolationError and raise
HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Username or merchant
ID already exists"), and add the necessary import for
asyncpg.exceptions.UniqueViolationError if not present.

In `@app/database/queries/breeze_buddy/users.py`:
- Around line 112-114: The current except block in check_username_exists
swallows DB errors and returns False, which can let the create flow continue;
change the error handling in the check_username_exists function so that on
database exceptions you either (a) return True (treat as "exists"/deny create)
or (b) re-raise the exception to surface the failure to callers like
create_merchant; update the logger.error call to include context (username and
the caught exception) before returning True or raising, and ensure
create_merchant handles the propagated exception if you choose re-raise.
🧹 Nitpick comments (6)
app/schemas/breeze_buddy/users.py (2)

40-40: Consider adding email format validation.

The email field accepts any string. Consider using Pydantic's EmailStr type for automatic email format validation.

♻️ Proposed fix
+from pydantic import BaseModel, EmailStr, Field
-from pydantic import BaseModel, Field

-    email: Optional[str] = Field(None, description="Email address of the merchant")
+    email: Optional[EmailStr] = Field(None, description="Email address of the merchant")

33-35: Consider adding length constraints to merchant_id.

The username field has min_length=3, max_length=50 constraints, but merchant_id (which is also a unique business identifier) has no such validation. This could allow empty strings or excessively long values.

♻️ Proposed fix
     merchant_id: str = Field(
-        ..., description="Primary business identifier (e.g., 'redbus')"
+        ..., min_length=1, max_length=100, description="Primary business identifier (e.g., 'redbus')"
     )
app/database/queries/breeze_buddy/users.py (2)

212-214: Consider using spread syntax for list concatenation.

Minor style improvement using Python's spread operator.

♻️ Proposed fix
     # Ensure primary merchant_id is in the list
     elif merchant_id not in merchant_ids:
-        merchant_ids = [merchant_id] + merchant_ids
+        merchant_ids = [merchant_id, *merchant_ids]

29-43: Consider logging a warning when merchant_ids is empty.

If merchant_ids is empty (possibly due to data migration issues), merchant_id becomes an empty string. While create_merchant prevents this for new records, adding a warning would help detect data integrity issues.

♻️ Proposed defensive check
 def _build_merchant_response(row) -> MerchantResponse:
     """Build MerchantResponse from database row."""
     merchant_ids = _parse_json_field(row["merchant_ids"])
+    if not merchant_ids:
+        logger.warning(f"Merchant {row['id']} has empty merchant_ids")
     return MerchantResponse(
         id=str(row["id"]),
app/api/routers/breeze_buddy/__init__.py (2)

20-20: Naming inconsistency: users_router serves merchant endpoints.

The router is imported as users_router but serves endpoints tagged as ["merchants"]. This was flagged in PR comments. Consider renaming to merchant_users_router or adjusting the module name to better reflect its purpose, though this is a minor consistency concern.

♻️ Option A: Rename import alias
-from app.api.routers.breeze_buddy.users import router as users_router
+from app.api.routers.breeze_buddy.users import router as merchants_router

-router.include_router(users_router, prefix="", tags=["merchants"])
+router.include_router(merchants_router, prefix="", tags=["merchants"])

Also applies to: 48-49


44-46: Consider removing commented-out code.

The commented-out router.include_router(merchants_router, ...) line can be removed since the explanatory comment above it already documents the change. Keeping dead code (even commented) can cause confusion.

♻️ Proposed cleanup
-# NOTE: Old merchants_router removed - conflicts with users_router which now handles
-# merchant CRUD at /merchants, /merchant, /merchant/{id} endpoints
-# router.include_router(merchants_router, prefix="", tags=["merchants"])
-
+# Merchant CRUD now handled by users_router at /merchants, /merchant, /merchant/{id} endpoints
 # Users/Merchants (merchant management - RBAC enabled)
 router.include_router(users_router, prefix="", tags=["merchants"])

@narsimhaReddyJuspay
Copy link
Contributor Author

@coderabbitai review once more

@coderabbitai
Copy link

coderabbitai bot commented Feb 3, 2026

@narsimhaReddyJuspay, I'll conduct another comprehensive review of all the changes in this PR, including the merchant management endpoints, database queries, API schemas, router configurations, and all related code.

✅ Actions performed

Full review triggered.

@narsimhaReddyJuspay narsimhaReddyJuspay force-pushed the add-user-crud-endpoints branch 6 times, most recently from 630ee89 to 9d311aa Compare February 4, 2026 12:01
@narsimhaReddyJuspay
Copy link
Contributor Author

@coderabbitai i have added a lot of new things review again carefully

@coderabbitai
Copy link

coderabbitai bot commented Feb 4, 2026

@narsimhaReddyJuspay, I'll conduct a comprehensive review of all the changes in this PR, including any new additions you've made.

✅ Actions performed

Full review triggered.

@narsimhaReddyJuspay
Copy link
Contributor Author

@coderabbitai fixed the previous comments once review the entire code carefully again

@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

@narsimhaReddyJuspay, thank you for addressing the previous comments! I'll conduct another comprehensive review of the entire codebase, examining all aspects including:

  • Architecture: Merchant entities vs. user accounts separation, RBAC implementation, owner tracking
  • Security: Authentication, authorization, password hashing, input validation, error handling
  • Database: Migration 014, JSONB handling, queries, indexes, transaction safety
  • API: All CRUD endpoints, schemas, error responses, audit logging
  • Code Quality: Handler/query separation, documentation, naming consistency

I'll provide detailed feedback on the implementation.

✅ Actions performed

Full review triggered.

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/database/queries/breeze_buddy/users.py (1)

94-114: ⚠️ Potential issue | 🟡 Minor

Inline JSON parsing bypasses _parse_json_field safety nets.

Lines 100-108 manually parse merchant_ids and shop_identifiers without handling None values or JSONDecodeError. If either column is NULL in the DB, json.loads(None) raises TypeError. The _parse_json_field helper defined at line 22 handles both cases. Use it here for consistency and safety.

🛡️ Proposed fix
             return UserInDB(
                 id=str(row["id"]),
                 username=row["username"],
                 password_hash=row["password_hash"],
                 role=UserRole(row["role"]),
                 email=row["email"],
-                merchant_ids=(
-                    row["merchant_ids"]
-                    if isinstance(row["merchant_ids"], list)
-                    else json.loads(row["merchant_ids"])
-                ),
-                shop_identifiers=(
-                    row["shop_identifiers"]
-                    if isinstance(row["shop_identifiers"], list)
-                    else json.loads(row["shop_identifiers"])
-                ),
+                merchant_ids=_parse_json_field(row["merchant_ids"]),
+                shop_identifiers=_parse_json_field(row["shop_identifiers"]),
                 is_active=row["is_active"],
                 owner_id=str(row["owner_id"]) if row.get("owner_id") else None,
                 created_at=row["created_at"],
                 updated_at=row["updated_at"],
             )
🤖 Fix all issues with AI agents
In `@app/api/routers/breeze_buddy/merchant_entities/handlers.py`:
- Around line 238-261: Move the call to get_merchant_entity_by_merchant_id into
the existing try/except so any non-HTTP exceptions are caught and logged;
specifically, wrap both merchant = await
merchant_queries.get_merchant_entity_by_merchant_id(merchant_id) and the
subsequent await merchant_queries.update_merchant_entity(...) inside the same
try block, keep the existing HTTPException re-raise, and ensure
logger.error(...) is used in the general except to log the error before raising
HTTPException(500) — this ensures both get_merchant_entity_by_merchant_id and
update_merchant_entity failures are handled consistently.

In `@app/api/routers/breeze_buddy/users/handlers.py`:
- Around line 192-224: The handler currently treats any False from
delete_merchant as a 404, which masks DB errors; update the data layer and
handler: change delete_merchant (in users.py) to raise on DB/execution errors
instead of returning False so callers can distinguish "not found" from real
errors, and in delete_merchant_handler wrap the await
delete_merchant(merchant_uuid) call in a try/except Exception to log the error
and raise an HTTPException(status_code=500) for unexpected DB failures while
preserving the existing 404 behavior when get_merchant_by_id(merchant_uuid)
returns None or when delete_merchant explicitly indicates a not-found outcome.
🧹 Nitpick comments (10)
app/core/security/authorization.py (1)

135-145: Function name is_shop is now misleading after the SHOP→USER rename.

The function checks UserRole.USER but is still called is_shop with a docstring referencing "shop-level user." Consider renaming to is_user (with an alias or deprecation wrapper for is_shop) to keep the API self-documenting. Otherwise callers will need tribal knowledge to understand the mapping.

♻️ Suggested rename
-def is_shop(current_user: UserInfo) -> bool:
+def is_user(current_user: UserInfo) -> bool:
     """
-    Check if user is a shop-level user.
+    Check if user is a basic (user-level) user.
 
     Args:
         current_user: Current authenticated user
 
     Returns:
-        True if user is shop, False otherwise
+        True if user has the USER role, False otherwise
     """
     return current_user.role == UserRole.USER
+
+
+# Backward-compatible alias
+is_shop = is_user
app/core/security/jwt.py (1)

92-106: Minor: manual expiry check is redundant with PyJWT's built-in check.

jwt.decode() (line 90) already raises jwt.ExpiredSignatureError for expired tokens (caught on line 119). The manual check on lines 93-106 is belt-and-suspenders. This is pre-existing behavior and not introduced by this PR, so no action needed now — just flagging for awareness.

app/schemas/breeze_buddy/merchant_entities.py (1)

28-28: is_active should be bool, not Optional[bool], on create.

During entity creation, is_active should always resolve to a concrete boolean. The Optional wrapper allows None to be passed explicitly, which is likely unintended for a create schema.

🔧 Suggested fix
-    is_active: Optional[bool] = Field(True, description="Active status (default: true)")
+    is_active: bool = Field(True, description="Active status (default: true)")
app/schemas/breeze_buddy/users.py (1)

39-39: No email format validation on email fields across all schemas.

All email fields are plain Optional[str] with no format validation. Invalid email strings (e.g. "not-an-email") will be accepted. Consider using Pydantic's EmailStr (from pydantic[email]) for basic format enforcement.

Also applies to: 58-58, 112-112, 131-131

app/database/migrations/014_create_merchants_table.sql (1)

7-16: Consider wrapping the migration in an explicit transaction.

This migration performs DDL (CREATE TABLE, ALTER TABLE) and DML (UPDATE) across two tables. If the migration framework doesn't auto-wrap in a transaction, a partial failure could leave the database in an inconsistent state (e.g., roles updated but constraint not yet applied).

app/schemas/breeze_buddy/__init__.py (1)

54-66: Dual UserCreate schemas may confuse consumers — consider documenting the migration path.

UserCreate from auth.py (line 26) and UserAccountCreate aliased from users.py (line 61) both model user creation but with different fields. The __all__ exports both. Downstream consumers importing from this package might pick the wrong one. A brief docstring note at the top of this file (or inline comments in __all__) clarifying which to use for new code would help.

app/database/queries/breeze_buddy/merchant_entities.py (1)

95-117: Missing no-connection raise in create_merchant_entity, get_merchant_entity_by_merchant_id, and update_merchant_entity.

These three functions silently return None if the async for loop completes without yielding, while check_merchant_id_exists (line 61), get_all_merchant_entities (line 233), and get_merchant_entities_by_ids (line 271) all explicitly raise Exception("No database connection available"). Align the pattern for consistency.

🛡️ Proposed fix for create_merchant_entity (apply similar to get_by_merchant_id and update)
     try:
         async for conn in get_db_connection():
             row = await conn.fetchrow(
                 query,
                 merchant_id,
                 name,
                 description,
                 shop_ids_json,
                 is_active,
                 owner_id,
                 now,
                 now,
             )

             if row:
                 logger.info(f"Created merchant entity: {merchant_id}")
                 return _build_merchant_entity_response(row)

             return None
+        raise Exception("No database connection available")

     except Exception as e:
         logger.error(f"Error creating merchant entity {merchant_id}: {e}")
         raise
app/api/routers/breeze_buddy/user_accounts/handlers.py (1)

245-267: Add except HTTPException: raise for consistency with other handlers.

Every other handler in this file (create_user_handler, get_user_by_id_handler, update_user_handler, delete_user_handler) re-raises HTTPException before the catch-all except Exception. This handler is missing that guard. While the current try-block doesn't raise HTTPException, adding it prevents accidental swallowing if the code evolves.

♻️ Proposed fix
     try:
         users, total = await user_queries.get_all_users(
             ...
         )
 
         total_pages = (total + limit - 1) // limit if total > 0 else 0
 
         return UserListResponse(
             users=users, total=total, page=page, limit=limit, total_pages=total_pages
         )
 
+    except HTTPException:
+        raise
     except Exception as e:
         logger.error(f"Error fetching users: {e}")
         raise HTTPException(status_code=500, detail="Internal server error")
app/api/routers/breeze_buddy/users/__init__.py (1)

88-108: Path parameter id shadows Python's built-in id().

The path parameter is named id in the get_merchant, update_merchant, and delete_merchant endpoints. While this works at runtime, it shadows the built-in and reduces readability. Consider renaming to merchant_uuid to match the handler parameter naming.

♻️ Proposed fix (apply to all three endpoints)
-@router.get("/merchant/{id}", response_model=MerchantResponse)
+@router.get("/merchant/{merchant_uuid}", response_model=MerchantResponse)
 async def get_merchant(
-    id: str,
+    merchant_uuid: str,
     current_user: UserInfo = Depends(get_current_user_with_rbac),
 ):
     ...
-    return await get_merchant_handler(current_user, id)
+    return await get_merchant_handler(current_user, merchant_uuid)
app/api/routers/breeze_buddy/merchant_entities/handlers.py (1)

126-169: Reseller path fetches all scoped merchants into memory before filtering.

For resellers with a large number of scoped merchants, get_merchant_entities_by_ids loads all of them into memory, then applies filters, sorting, and pagination in Python (lines 132–169). This works but won't scale well.

Consider extending get_merchant_entities_by_ids (or adding a new query) to accept filter/sort/pagination parameters and push this work to the database, similar to the admin path.

@narsimhaReddyJuspay narsimhaReddyJuspay force-pushed the add-user-crud-endpoints branch 2 times, most recently from 5e7c294 to 51ce9d8 Compare February 11, 2026 10:10
@narsimhaReddyJuspay
Copy link
Contributor Author

@coderabbitai review once more

@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

@narsimhaReddyJuspay, I'll conduct another comprehensive review of all the code changes in this PR, including the merchant and user management endpoints, RBAC implementation, database queries, schemas, router configurations, and the migration.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@app/api/routers/breeze_buddy/merchant_entities/handlers.py`:
- Around line 235-239: The view-access check is done after fetching the merchant
and thus leaks existence by returning 404 for missing and 403 for existing
resources; move the access check earlier: call
_check_merchant_view_access(current_user, merchant_id) before performing the DB
lookup that sets merchant (the variable named merchant) so unauthorized callers
hit the access check first, and then keep the existing raise
HTTPException(status_code=404, detail="Merchant entity not found") after the DB
read for users that passed the access check.

In `@app/api/routers/breeze_buddy/user_accounts/handlers.py`:
- Around line 319-322: The DB call to user_queries.get_user_by_id(user_id) is
outside the try/except so DB errors aren't being caught or logged; move that
call (and the subsequent check that raises HTTPException("User not found"))
inside the existing try/except used by this handler, and in the except block log
the exception (using the same structured logger as other handlers) before
re-raising or returning the appropriate HTTPException to preserve consistent
error handling.

In `@app/database/queries/breeze_buddy/merchant_entities.py`:
- Around line 131-142: Both get_merchant_entity_by_merchant_id and
update_merchant_entity currently return None when get_db_connection() yields
nothing; after the async for loop in each function (the blocks using `async for
conn in get_db_connection():`), add a fallback that raises Exception("No
database connection available") to match the pattern used elsewhere in this
module and ensure a clear error is thrown when no DB connection is obtained.
- Around line 95-117: The create_merchant_entity function currently falls
through and returns None if get_db_connection() yields no connection; change it
to raise a specific error when no DB connection is acquired (consistent with
check_merchant_id_exists, get_all_merchant_entities,
get_merchant_entities_by_ids, and delete_merchant_entity). Inside
create_merchant_entity, after the async for get_db_connection() loop, detect the
case where the loop never yielded a connection and raise a RuntimeError (or a
shared custom DBConnectionError) with a clear message like "No DB connection
available for create_merchant_entity"; ensure existing exception handling
remains for other errors and that the raised error replaces the implicit None
return.

In `@app/database/queries/breeze_buddy/users.py`:
- Line 690: The delete_user function currently accepts caller_role but never
uses it, so either remove the parameter or enforce RBAC: when implementing the
admin-deletion guard inside delete_user (e.g., where you check the target user's
role), also verify the caller_role (the function parameter) is UserRole.ADMIN
before allowing deletion; if caller_role is not ADMIN return False/raise a
Permission error. Update the signature only if you choose removal, otherwise add
the check using the caller_role parameter (and keep references to delete_user
and UserRole to locate the change).
- Around line 200-216: get_merchant_by_id currently passes merchant_uuid
directly to the DB and can raise asyncpg DataError for invalid UUIDs; mirror
get_user_by_id by validating the UUID first with _is_valid_uuid and return None
if invalid. Update the get_merchant_by_id function to call
_is_valid_uuid(merchant_uuid) before running the query, short-circuit with a
None return when validation fails, and keep the existing try/except logging
behavior (logger.error and re-raise) unchanged.

In `@MERCHANT_USER_MANAGEMENT_GUIDE.md`:
- Around line 105-111: Documentation incorrectly states there is "NO delete
endpoint" for merchant entities; update the Soft Delete section to reflect that
a delete endpoint exists and describe its behavior: reference the existing
delete_merchant_entity_handler (in
app/api/routers/breeze_buddy/merchant_entities/handlers.py) and the legacy
DELETE /merchant/{id} route, clarify whether those endpoints perform a hard
delete or set is_active=false, and align the guidance (e.g., prefer PUT
is_active=false for deactivation if delete performs hard removal) so the text
accurately matches the implementation and expected RBAC/behavior.
🧹 Nitpick comments (8)
app/api/routers/breeze_buddy/user_accounts/handlers.py (2)

371-412: Self-deletion check at Line 387 is redundant for admins.

Line 374 already blocks admin self-deletion. For non-admin users, Line 387 catches self-deletion. However, if a non-admin (e.g., reseller) reaches Line 387 with user_id == current_user.id, _check_update_delete_access at Line 391 would also need to handle this — but actually _check_update_delete_access doesn't block self-modification. The flow is fine but the ordering is slightly confusing.

More importantly: the same DB-call-outside-try issue applies here — get_user_by_id at Line 378 is outside the try at Line 393. Consider wrapping Lines 378–391 inside the try block for consistency.


265-267: HTTPException raised inside try is swallowed by bare except Exception.

In get_all_users_handler, unlike the other handlers (e.g., get_user_by_id_handler at Lines 308-309), there is no except HTTPException: raise clause before the broad except Exception. If user_queries.get_all_users raises an HTTPException (unlikely from query layer but possible in future), it would be masked as a 500.

This is low-risk but inconsistent with the pattern in the other four handlers.

♻️ Add HTTPException passthrough for consistency
     try:
         users, total = await user_queries.get_all_users(
             ...
         )
         ...
+    except HTTPException:
+        raise
     except Exception as e:
         logger.error(f"Error fetching users: {e}")
         raise HTTPException(status_code=500, detail="Internal server error")
app/api/routers/breeze_buddy/merchant_entities/handlers.py (1)

285-321: delete_merchant_entity_handler returns a plain dict instead of a typed response model.

Other delete handlers (e.g., delete_user_handler) return DeleteResponse. Returning an untyped dict bypasses Pydantic serialization/validation and makes the OpenAPI schema less useful. Consider using DeleteResponse for consistency.

♻️ Suggested change
+from app.schemas.breeze_buddy.users import DeleteResponse
+
 async def delete_merchant_entity_handler(
     merchant_id: str, current_user: UserInfo
-) -> dict:
+) -> DeleteResponse:
     ...
-        return {
-            "success": True,
-            "message": f"Merchant entity '{merchant_id}' deleted",
-            "deleted_id": merchant_id,
-        }
+        return DeleteResponse(
+            success=True,
+            message=f"Merchant entity '{merchant_id}' deleted",
+            deleted_id=merchant_id,
+        )
app/api/routers/breeze_buddy/users/__init__.py (1)

88-108: Path parameter id shadows Python builtin.

The path parameter is named id in multiple endpoints (Lines 90, 113, 138). While common in FastAPI examples, it shadows Python's built-in id(). Consider renaming to merchant_id for clarity and to match the domain language.

♻️ Example rename
-@router.get("/merchant/{id}", response_model=MerchantResponse)
+@router.get("/merchant/{merchant_id}", response_model=MerchantResponse)
 async def get_merchant(
-    id: str,
+    merchant_id: str,
     current_user: UserInfo = Depends(get_current_user_with_rbac),
 ):
     ...
-    return await get_merchant_handler(current_user, id)
+    return await get_merchant_handler(current_user, merchant_id)
app/schemas/breeze_buddy/users.py (1)

105-124: Consolidate duplicate UserCreate/UserUpdate schemas across modules.

app/schemas/breeze_buddy/auth.py defines UserCreate (lines 115–123) and UserUpdate (lines 127–134) with significantly different field constraints than their counterparts in users.py:

  • username and password lack validation constraints (no min_length)
  • shop_identifiers defaults to list instead of lambda: ["*"]
  • is_active uses direct assignment vs Field()

While the application correctly imports from users.py, maintaining identically-named classes with different validation rules in sibling modules is unmaintainable and creates silent error risk. Remove the auth.py versions or rename one pair (e.g., AuthUserCreate / AuthUserUpdate) for clarity.

app/core/security/authorization.py (1)

135-145: Function name is_shop is now misleading after the role rename to UserRole.USER.

The function body correctly checks UserRole.USER, but the function name and docstring still reference the old "shop" concept. Consider renaming to is_user for consistency with the enum rename and improved clarity.

♻️ Suggested rename
-def is_shop(current_user: UserInfo) -> bool:
+def is_user(current_user: UserInfo) -> bool:
     """
-    Check if user is a shop-level user.
+    Check if user is a basic user-level account.
 
     Args:
         current_user: Current authenticated user
 
     Returns:
-        True if user is shop, False otherwise
+        True if user has the USER role, False otherwise
     """
     return current_user.role == UserRole.USER
app/database/queries/breeze_buddy/users.py (2)

103-118: get_user_by_username manually parses JSON instead of using _parse_json_field.

Lines 109-118 inline the JSON parsing logic for merchant_ids and shop_identifiers, but _build_user_response and _build_merchant_response use the _parse_json_field helper (which also handles JSONDecodeError). This inconsistency means malformed JSON here would raise an unhandled exception instead of falling back gracefully.

♻️ Proposed fix
             return UserInDB(
                 id=str(row["id"]),
                 username=row["username"],
                 password_hash=row["password_hash"],
                 role=UserRole(row["role"]),
                 email=row["email"],
-                merchant_ids=(
-                    row["merchant_ids"]
-                    if isinstance(row["merchant_ids"], list)
-                    else json.loads(row["merchant_ids"])
-                ),
-                shop_identifiers=(
-                    row["shop_identifiers"]
-                    if isinstance(row["shop_identifiers"], list)
-                    else json.loads(row["shop_identifiers"])
-                ),
+                merchant_ids=_parse_json_field(row["merchant_ids"]),
+                shop_identifiers=_parse_json_field(row["shop_identifiers"]),
                 is_active=row["is_active"],
                 owner_id=str(row["owner_id"]) if row.get("owner_id") else None,
                 created_at=row["created_at"],
                 updated_at=row["updated_at"],
             )

31-39: _parse_json_field is duplicated across query modules.

This identical function exists in both this file and app/database/queries/breeze_buddy/merchant_entities.py (lines 18-26). Consider extracting it into a shared utility (e.g., app/database/queries/breeze_buddy/utils.py) to avoid drift.

@narsimhaReddyJuspay narsimhaReddyJuspay force-pushed the add-user-crud-endpoints branch 2 times, most recently from a4b2a09 to 4071e5d Compare February 11, 2026 10:29
@narsimhaReddyJuspay
Copy link
Contributor Author

@coderabbitai review once more

@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

@narsimhaReddyJuspay, I'll review all the code changes in this PR once more.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/database/queries/breeze_buddy/users.py (1)

103-123: ⚠️ Potential issue | 🟠 Major

get_user_by_username uses raw json.loads instead of _parse_json_field — risks JSONDecodeError.

Lines 109-118 parse merchant_ids and shop_identifiers with bare json.loads, bypassing the _parse_json_field helper (line 31) that was introduced specifically to handle malformed JSON gracefully. If a row contains corrupted JSON, this will crash the authentication flow.

🐛 Proposed fix
             return UserInDB(
                 id=str(row["id"]),
                 username=row["username"],
                 password_hash=row["password_hash"],
                 role=UserRole(row["role"]),
                 email=row["email"],
-                merchant_ids=(
-                    row["merchant_ids"]
-                    if isinstance(row["merchant_ids"], list)
-                    else json.loads(row["merchant_ids"])
-                ),
-                shop_identifiers=(
-                    row["shop_identifiers"]
-                    if isinstance(row["shop_identifiers"], list)
-                    else json.loads(row["shop_identifiers"])
-                ),
+                merchant_ids=_parse_json_field(row["merchant_ids"]),
+                shop_identifiers=_parse_json_field(row["shop_identifiers"]),
                 is_active=row["is_active"],
                 owner_id=str(row["owner_id"]) if row.get("owner_id") else None,
                 created_at=row["created_at"],
                 updated_at=row["updated_at"],
             )
🤖 Fix all issues with AI agents
In `@app/api/routers/breeze_buddy/merchant_entities/__init__.py`:
- Around line 33-57: The docstring and handler are inconsistent about
duplicate-merchant errors—update the duplicate-merchant response to use 409
Conflict to match create_user_handler: change the HTTPException raised in
create_merchant_entity_handler from status_code=400 to status_code=409 and
update the create_merchant_entity endpoint docstring (and any related tests or
assertions) to state "409: If merchant_id already exists"; keep the handler name
(create_merchant_entity_handler) and endpoint function (create_merchant_entity)
as references when making the changes.

In `@app/api/routers/breeze_buddy/user_accounts/handlers.py`:
- Around line 245-267: The get_all_users_handler currently catches all
exceptions and converts them to a 500, which will swallow any HTTPException;
modify the error handling in get_all_users_handler so it first re-raises
HTTPException (add an "except HTTPException: raise" clause immediately before
the generic "except Exception as e"), then keep the logger.error and raise
HTTPException(status_code=500, detail="Internal server error") for other
exceptions; locate the handler around the call to user_queries.get_all_users and
the return of UserListResponse to add this clause.

In `@app/api/routers/breeze_buddy/users/handlers.py`:
- Around line 80-134: Move the DB pre-check calls into the existing try/except
in create_merchant_handler: include
check_username_exists(merchant_data.username) and
check_merchant_id_exists(merchant_data.merchant_id) inside the try block along
with create_merchant, so any DB errors they raise are caught; preserve the
existing except asyncpg.UniqueViolationError branch that inspects the exception
for "username" and the subsequent HTTPException mappings, and ensure you still
raise a 500 HTTPException if merchant is falsy and log via logger.info on
success.

In `@app/schemas/breeze_buddy/users.py`:
- Around line 105-124: Align the package exports so the constrained model in
users.py becomes the canonical UserCreate: update the re-exporting in
app/schemas/breeze_buddy/__init__.py and the top-level app/schemas (and
app/schemas/__init__.py or app/schemas.py if present) to import and expose
breeze_buddy.users.UserCreate as UserCreate (instead of
breeze_buddy.auth.UserCreate); remove or rename the unconstrained
auth.py::UserCreate (or export it under a distinct name like AuthUserCreate) to
avoid duplicate/conflicting symbols so all consumers importing
app.schemas.UserCreate get the constrained version used by handlers.

In `@MERCHANT_USER_MANAGEMENT_GUIDE.md`:
- Around line 366-384: Docs show truthy checks and defaulting for JSON fields
but the implementation uses explicit None checks; update the examples in
_parse_json_field and _build_merchant_entity_response to match the real code: in
_parse_json_field describe/illustrate returning value if value is not None else
[], and in _build_merchant_entity_response use an explicit "is not None" check
for shop_identifiers (and any owner_id handling) instead of a general truthiness
check so the docs reflect the actual behavior of the functions named
_parse_json_field and _build_merchant_entity_response.
- Around line 779-783: The test example incorrectly asserts that
UserRole.RESELLER is denied by _check_create_merchant_access; update the example
so it matches the RBAC matrix and implementation by either changing the role to
one that is actually denied (e.g., use UserRole.MERCHANT in
test_reseller_cannot_create_merchant) or change the assertion to expect success
for RESELLER (remove pytest.raises and assert no HTTPException), referencing the
test function test_reseller_cannot_create_merchant and the access-checking
function _check_create_merchant_access to locate the change.
🧹 Nitpick comments (9)
app/database/queries/breeze_buddy/merchant_entities.py (2)

19-27: Consider extracting _parse_json_field into a shared utility.

This helper is duplicated verbatim in app/database/queries/breeze_buddy/users.py (lines 30-38). Extract it into a shared module (e.g., app/database/queries/breeze_buddy/utils.py) to avoid drift between the two copies.


277-342: update_merchant_entity does not allow clearing description to None.

All fields use if field is not None guards, which means a caller can never explicitly set description back to None/NULL once it has a value. If clearing optional fields is a valid use case, consider a different sentinel or an explicit "clear" mechanism. If not needed now, this is fine to defer.

app/api/routers/breeze_buddy/merchant_entities/handlers.py (2)

150-196: Reseller path fetches all scoped merchants into memory before filtering.

For resellers, get_merchant_entities_by_ids loads every merchant in their scope, then filtering, sorting, and pagination are applied in Python (lines 158-195). If a reseller has a large scope this is O(n) memory and CPU. Consider pushing filters into the DB query — e.g., adding filter parameters to get_merchant_entities_by_ids, or using get_all_merchant_entities with a merchant_id-IN constraint.


285-327: Delete handler duplicates the role-check logic from _check_update_access.

Lines 290-294 and 305-310 replicate the same admin/reseller + ownership check that _check_update_access already implements. Reuse it to keep the authorization logic DRY and avoid divergence.

Suggested refactor
 async def delete_merchant_entity_handler(
     merchant_id: str, current_user: UserInfo
 ) -> dict:
     """Delete merchant entity (admin or reseller who owns it)."""
-    # Only admin and reseller can delete
-    if current_user.role not in [UserRole.ADMIN, UserRole.RESELLER]:
-        raise HTTPException(
-            status_code=403,
-            detail="Only admin or reseller can delete merchant entities",
-        )
-
     try:
         # Get the merchant first to check ownership
         merchant = await merchant_queries.get_merchant_entity_by_merchant_id(
             merchant_id
         )
         if not merchant:
             raise HTTPException(status_code=404, detail="Merchant entity not found")
 
-        # Resellers can only delete merchants they own
-        if current_user.role == UserRole.RESELLER:
-            if merchant.owner_id != current_user.id:
-                raise HTTPException(
-                    status_code=403,
-                    detail="You can only delete merchant entities you created",
-                )
+        # Check update/delete access (admin can delete any, reseller only their own)
+        _check_update_access(current_user, merchant.owner_id)
 
         deleted = await merchant_queries.delete_merchant_entity(merchant_id)
app/api/routers/breeze_buddy/users/__init__.py (1)

88-108: Path parameter id shadows the Python builtin.

Using id as a parameter name works but shadows the built-in id() function. Consider merchant_id or user_id for clarity — but this is a minor nit for a legacy endpoint.

app/schemas/breeze_buddy/__init__.py (1)

54-66: Multiple import statements from the same module could be consolidated.

Lines 54-60, 61, 62-65, and 66 all import from app.schemas.breeze_buddy.users. The aliases (UserAccountCreate, UserAccountUpdate) require separate import statements, but the non-aliased imports (lines 54-60 and 62-65) could be merged into one. Minor readability nit.

app/api/routers/breeze_buddy/user_accounts/handlers.py (1)

209-217: Add exception chaining with from clauses for clearer tracebacks.

Ruff B904 flags several re-raises in except clauses without from. This makes it harder to trace the root cause during debugging. The pattern applies here and in the other handlers (lines 267, 312, 368, 410, 415).

♻️ Proposed fix for this handler
     except asyncpg.UniqueViolationError:
         raise HTTPException(
             status_code=409, detail=f"Username '{user_data.username}' already exists"
-        )
+        ) from None
     except HTTPException:
         raise
     except Exception as e:
         logger.error(f"Error creating user: {e}")
-        raise HTTPException(status_code=500, detail="Internal server error")
+        raise HTTPException(status_code=500, detail="Internal server error") from e
app/api/routers/breeze_buddy/merchant_entities/__init__.py (1)

169-195: Handler return type dict vs router response_model=DeleteResponse.

The delete_merchant_entity_handler has return type -> dict and returns a plain dict, while this route declares response_model=DeleteResponse. FastAPI handles this at runtime (Pydantic validates the dict shape), but the type mismatch can confuse type checkers and developers. Consider updating the handler to return a DeleteResponse instance for consistency — the user_accounts delete_user_handler already does this correctly.

app/api/routers/breeze_buddy/users/handlers.py (1)

137-224: Missing error handling around DB calls in get/update/delete handlers.

create_merchant_handler wraps the insert in try/except for structured logging and graceful error conversion, but get_merchant_handler, update_merchant_handler, and delete_merchant_handler let exceptions from get_merchant_by_id, update_merchant, and delete_merchant propagate unhandled. While the query layer logs before re-raising, unhandled exceptions here bypass handler-level audit context (e.g., which admin triggered the failing operation).

The user_accounts handlers consistently wrap all DB calls. Consider applying the same pattern here for consistency.

♻️ Example for get_merchant_handler
 async def get_merchant_handler(
     current_user: UserInfo,
     merchant_uuid: str,
 ) -> MerchantResponse:
     """GET /merchant/{id} - Get merchant by UUID. Admin only."""
     require_admin(current_user)
 
+    try:
         merchant = await get_merchant_by_id(merchant_uuid)
 
         if not merchant:
             raise HTTPException(
                 status_code=status.HTTP_404_NOT_FOUND,
                 detail=f"Merchant with UUID '{merchant_uuid}' not found",
             )
 
         return merchant
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"Error fetching merchant {merchant_uuid}: {e}")
+        raise HTTPException(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            detail="Internal server error",
+        ) from e

@narsimhaReddyJuspay
Copy link
Contributor Author

@coderabbitai review once more

@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

@narsimhaReddyJuspay, I'll review all the code changes in this PR once more.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@app/api/routers/breeze_buddy/merchant_entities/__init__.py`:
- Around line 138-166: The docstring for update_merchant_entity incorrectly
states "Admin-only" and lists a 403 for non-admins, but the actual RBAC (via
get_current_user_with_rbac and update_merchant_entity_handler) allows reseller
owners; either update the docstring/Raises section to reflect that reseller
owners and admins can perform updates, or enforce admin-only access by changing
the RBAC check used here and/or inside update_merchant_entity_handler (replace
or tighten get_current_user_with_rbac behavior to require admin role). Ensure
references to update_merchant_entity and update_merchant_entity_handler are
updated accordingly so docs match actual permission logic.

In `@app/api/routers/breeze_buddy/merchant_entities/handlers.py`:
- Around line 56-88: The RBAC functions _get_merchant_ids_filter and
_check_merchant_view_access currently allow any user with scoped merchant_ids to
list/view entities; restrict access to admin/reseller only by adding a role
guard: in both _get_merchant_ids_filter(current_user: UserInfo) and
_check_merchant_view_access(current_user: UserInfo, merchant_id: str) first
check if current_user.role is in {UserRole.ADMIN, UserRole.RESELLER} and only
then apply the existing wildcard/merchant_ids logic, otherwise return None for
the filter or raise HTTPException(status_code=403) in the checker;
alternatively, if merchant/user access is intended, update the router docs to
explicitly state that non-admin/reseller roles with scoped merchant_ids can view
entities.

In `@app/api/routers/breeze_buddy/user_accounts/__init__.py`:
- Around line 33-57: The route docstring for create_user_account is missing the
409 response for username conflicts; update the docstring for
create_user_account (above the function) to list 409 as a possible response when
the username already exists (to match create_user_handler's behavior), e.g., add
a "409: If username already exists" entry in the Raises/Returns section and
ensure the description aligns with create_user_handler's duplicate-user
behavior.

In `@app/api/routers/breeze_buddy/user_accounts/handlers.py`:
- Around line 185-191: Move the pre-check call to
user_queries.check_username_exists(user_data.username) into the existing
try/except block that wraps other DB operations in this handler so that any
exceptions are logged and mapped to an HTTPException consistently; specifically,
call check_username_exists inside the try, catch exceptions in the handler's
except block (or re-use its logging/error mapping) and only raise the 409
HTTPException when the call returns True—ensure you reference
user_queries.check_username_exists and user_data.username when relocating the
call.

In `@app/database/queries/breeze_buddy/merchant_entities.py`:
- Around line 178-186: Escape SQL wildcard characters in the user-supplied
filters before wrapping with % and add an ESCAPE clause: for merchant_id_filter
and name_filter, first replace backslashes with double backslashes, then replace
'%' with '\%' and '_' with '\_' and then do params.append(f"%{escaped}%"); keep
the parameter index logic using param_idx, and change the corresponding
where_conditions entries to include "ESCAPE '\\'" (e.g. f"merchant_id ILIKE
${param_idx} ESCAPE '\\'") so the backslash escapes are honored.

In `@MERCHANT_USER_MANAGEMENT_GUIDE.md`:
- Around line 189-209: The flow diagram incorrectly shows the request as "POST
/merchant ..." — update the diagram to use the actual endpoint "POST
/entity/merchant ..." so it matches the real handler name
create_merchant_entity_handler() and the database flow; ensure any other
mentions of "/merchant" in this diagram block are replaced with
"/entity/merchant" for consistency.
🧹 Nitpick comments (6)
app/schemas/breeze_buddy/users.py (1)

70-87: MerchantResponse is missing owner_id field, unlike UserResponse.

UserResponse (Line 149) includes owner_id, but MerchantResponse does not. Since both legacy merchants and user accounts now have owner_id in the database (added by migration 014), the legacy merchant response model should also expose it for consistency and auditability.

♻️ Suggested addition
 class MerchantResponse(BaseModel):
     """Merchant response model."""
 
     id: str = Field(..., description="Merchant's UUID")
     username: str
     email: Optional[str] = None
     role: UserRole = UserRole.MERCHANT
     merchant_id: str = Field(
         ..., description="Primary business identifier (e.g., 'redbus')"
     )
     merchant_ids: List[str] = Field(
         default_factory=list,
         description="List of merchant identifiers for access control",
     )
     shop_identifiers: List[str] = Field(default_factory=list)
     is_active: bool = True
+    owner_id: Optional[str] = None
     created_at: Optional[datetime] = None
     updated_at: Optional[datetime] = None
app/api/routers/breeze_buddy/users/__init__.py (1)

88-108: Path parameter id shadows the Python built-in.

The parameter name id in get_merchant, update_merchant, and delete_merchant shadows Python's built-in id(). Consider renaming to merchant_id for clarity and to avoid the shadowing, which also aligns better with the new merchant entity endpoints that use merchant_id as the path parameter name.

♻️ Example for one endpoint (apply similarly to others)
-@router.get("/merchant/{id}", response_model=MerchantResponse)
+@router.get("/merchant/{merchant_id}", response_model=MerchantResponse)
 async def get_merchant(
-    id: str,
+    merchant_id: str,
     current_user: UserInfo = Depends(get_current_user_with_rbac),
 ):
app/core/security/authorization.py (1)

135-145: Function name is_shop is semantically misaligned after the role rename.

The UserRole.SHOP was renamed to UserRole.USER (as noted in the enum definition), but the function is_shop was not updated. It now checks UserRole.USER while maintaining the name and docstring referencing "shop-level user," creating confusion. Since this function is unused in the codebase, renaming to is_user with an updated docstring is a safe improvement.

♻️ Suggested rename
-def is_shop(current_user: UserInfo) -> bool:
+def is_user(current_user: UserInfo) -> bool:
     """
-    Check if user is a shop-level user.
+    Check if user has the 'user' role.
 
     Args:
         current_user: Current authenticated user
 
     Returns:
-        True if user is shop, False otherwise
+        True if user has the user role, False otherwise
     """
     return current_user.role == UserRole.USER
app/database/queries/breeze_buddy/merchant_entities.py (3)

19-27: Duplicate _parse_json_field — extract to a shared utility.

This function is identical to _parse_json_field in app/database/queries/breeze_buddy/users.py (lines 30–38). Having two copies means bug fixes or behavior changes must be applied in both places.

Consider extracting it into a shared helper module (e.g., app/database/queries/breeze_buddy/utils.py) and importing from both users.py and merchant_entities.py.


61-64: Consider using a custom exception class instead of bare Exception.

Every "no database connection available" path raises Exception(...), which makes it impossible for callers to distinguish a connection failure from other exceptions. A custom DatabaseConnectionError(RuntimeError) would allow targeted handling and aligns with Ruff TRY002/TRY003 hints.

♻️ Example
class DatabaseConnectionError(RuntimeError):
    """Raised when no database connection could be acquired."""
    pass

Then replace each raise Exception("No database connection available") with raise DatabaseConnectionError().

Also applies to: 114-117, 139-142, 233-237, 271-274, 339-342, 364-367


277-342: No way to explicitly clear description back to NULL.

Since the guard is if description is not None, passing None means "don't change" — but there's no mechanism for a caller to explicitly set description to NULL in the database. This is fine if the update schema uses Optional[str] with a default of None to mean "omitted", but verify the API contract supports this. If clearing fields is needed in the future, consider a sentinel like UNSET to distinguish "omitted" from "set to null."

@narsimhaReddyJuspay
Copy link
Contributor Author

@coderabbitai please review

@coderabbitai
Copy link

coderabbitai bot commented Feb 12, 2026

@narsimhaReddyJuspay, I'll review all the code changes in this PR.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/database/queries/breeze_buddy/users.py (1)

109-118: ⚠️ Potential issue | 🟡 Minor

Inline JSON parsing lacks JSONDecodeError handling — use _parse_json_field instead.

get_user_by_username manually parses merchant_ids and shop_identifiers with json.loads but without a try/except for JSONDecodeError. The _parse_json_field helper (Line 31) already handles this safely. Use it here for consistency and resilience against malformed data.

Proposed fix
             return UserInDB(
                 id=str(row["id"]),
                 username=row["username"],
                 password_hash=row["password_hash"],
                 role=UserRole(row["role"]),
                 email=row["email"],
-                merchant_ids=(
-                    row["merchant_ids"]
-                    if isinstance(row["merchant_ids"], list)
-                    else json.loads(row["merchant_ids"])
-                ),
-                shop_identifiers=(
-                    row["shop_identifiers"]
-                    if isinstance(row["shop_identifiers"], list)
-                    else json.loads(row["shop_identifiers"])
-                ),
+                merchant_ids=_parse_json_field(row["merchant_ids"]),
+                shop_identifiers=_parse_json_field(row["shop_identifiers"]),
                 is_active=row["is_active"],
                 owner_id=str(row["owner_id"]) if row.get("owner_id") else None,
                 created_at=row["created_at"],
                 updated_at=row["updated_at"],
             )
🤖 Fix all issues with AI agents
In `@app/database/queries/breeze_buddy/users.py`:
- Around line 426-451: The create_user, get_user_by_id, and update_user
functions currently return None if the async for over get_db_connection()
doesn't run (no DB connection); add the same fallback after the async for loop
as used in get_all_users and delete_merchant by raising Exception("No database
connection available") so the error is surfaced instead of silently returning
None; locate the logic around the async for conn in get_db_connection() blocks
inside create_user, get_user_by_id, and update_user and append the raise when no
connection yields a result (retain existing logger.error/raise in the except
block and keep using _build_user_response, get_db_connection, and logger
symbols).
- Around line 493-497: The username_filter handling in the users.py block (and
the similar logic in merchant_entities.py) only escapes '%' and '_' but not the
backslash, so literal '\' can be misinterpreted in the ILIKE pattern; update the
escape sequence to first replace '\' with '\\' (escape backslashes) before
replacing '%' and '_' and ensure the params use the escaped value (e.g., in the
block that builds where_conditions and params for username_filter in users.py
and the analogous code in merchant_entities.py).
- Around line 264-284: The create_merchant function currently returns None if
get_db_connection() yields no connections; add the same explicit raise used in
other functions (e.g., get_all_merchants, delete_merchant) after the async for
loop to surface a "no database connection" error. Locate create_merchant and the
async for conn in get_db_connection() block, and after the loop add a raised
exception (with a clear message) so missing DB connections don't silently return
None; preserve the existing except block that logs exceptions from the query
execution.
- Around line 341-349: The async function update_merchant iterates `async for
conn in get_db_connection():` but if the generator yields no connections it
silently falls through; add an explicit raise after the loop (similar to
create_merchant) to surface a "no database connection" error. Locate
update_merchant and, after the try block's async-for (but before the except),
raise a descriptive exception (e.g., RuntimeError or a custom error) like "No
database connection available for update_merchant {merchant_uuid}" so failures
when get_db_connection yields nothing are not swallowed.

In `@MERCHANT_USER_MANAGEMENT_GUIDE.md`:
- Around line 556-566: The docs use the placeholder /user/{id} for the GET and
PUT endpoints whereas the router exposes /user/{user_id}; update the API
documentation to use the same placeholder /user/{user_id} (for GET
/user/{user_id} and PUT /user/{user_id}) so it matches the actual route names
and avoid confusion with DELETE /user/{user_id}; ensure any references to 'id'
in the user endpoints (GET, PUT) are replaced with 'user_id' throughout the
section.
🧹 Nitpick comments (2)
app/database/queries/breeze_buddy/users.py (2)

151-164: check_merchant_id_exists name collision with merchant_entities.py.

This function checks the users table (JSONB ? operator on merchant_ids), while merchant_entities.py also exports a check_merchant_id_exists that checks the merchants table. Identical names with different semantics can cause subtle import bugs at call sites. Consider renaming one to clarify scope, e.g., check_merchant_id_exists_in_users.


31-39: _parse_json_field is duplicated in merchant_entities.py.

Both files define an identical _parse_json_field helper. Consider extracting it to a shared utility module (e.g., app/database/queries/breeze_buddy/utils.py) to avoid drift.

@narsimhaReddyJuspay
Copy link
Contributor Author

@coderabbitai please review

Copilot AI review requested due to automatic review settings February 18, 2026 08:32
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new “merchant entity vs login account” architecture for Breeze Buddy, introducing admin/reseller/merchant/user dashboard APIs with RBAC controls while keeping legacy merchant CRUD endpoints for compatibility.

Changes:

  • Introduces new Merchant Entity CRUD APIs backed by a new merchants table (with merchant_id as PK).
  • Adds RBAC-scoped User Account management APIs (CRUD + pagination/filtering/sorting) and tracks creator via owner_id.
  • Renames role SHOPUSER across schemas/auth and migrates existing DB rows + constraint.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
app/schemas/breeze_buddy/users.py Adds Pydantic schemas for legacy merchant CRUD and new user-account CRUD responses/requests.
app/schemas/breeze_buddy/merchant_entities.py Adds Pydantic schemas for merchant business entities.
app/schemas/breeze_buddy/auth.py Renames SHOPUSER and adds owner_id to user models.
app/schemas/breeze_buddy/init.py Re-exports new schemas for Breeze Buddy package consumers.
app/schemas/init.py Re-exports legacy merchant CRUD schemas at top-level schemas package.
app/database/queries/breeze_buddy/users.py Expands query layer to support legacy merchant CRUD and RBAC user-account CRUD; adds owner_id support.
app/database/queries/breeze_buddy/merchant_entities.py Implements query layer for merchants table CRUD + filtering/sorting.
app/database/migrations/014_create_merchants_table.sql Creates merchants table, adds owner_id to users, migrates role values, and updates role CHECK constraint.
app/core/security/authorization.py Updates is_shop role check to match renamed USER role.
app/api/routers/breeze_buddy/users/handlers.py Adds admin-only legacy merchant CRUD handlers (active for dashboard compatibility).
app/api/routers/breeze_buddy/users/init.py Adds legacy merchant CRUD routes (/merchants, /merchant*).
app/api/routers/breeze_buddy/user_accounts/handlers.py Adds RBAC business logic for user account CRUD and listing.
app/api/routers/breeze_buddy/user_accounts/init.py Adds user account routes (/user, /users, /user/{id}).
app/api/routers/breeze_buddy/merchant_entities/handlers.py Adds RBAC business logic for merchant entity CRUD and listing.
app/api/routers/breeze_buddy/merchant_entities/init.py Adds merchant entity routes under /entity/* to avoid legacy path conflicts.
app/api/routers/breeze_buddy/init.py Registers new routers and removes old merchants router in favor of legacy users router + new entity/user-account routers.
MERCHANT_USER_MANAGEMENT_GUIDE.md Adds a comprehensive guide covering architecture, RBAC, migration, deployment, and endpoint usage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@narsimhaReddyJuspay narsimhaReddyJuspay force-pushed the add-user-crud-endpoints branch 2 times, most recently from 4c1987d to 9a230b3 Compare February 19, 2026 20:03
- added new merchant entity endpoints
- added crud endpoints for user accounts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants