feat(tenancy): add merchant team CRUD with multi-store hub view
The merchant team page was read-only. Now merchant owners can invite, edit roles, and remove team members across all their stores from a single hub view. Architecture: No new models — delegates to existing store_team_service. Members are deduplicated across stores with per-store role badges. New: - 5 API endpoints: GET team (member-centric), GET store roles, POST invite (multi-store), PUT update role, DELETE remove member - merchant-team.js Alpine component with invite/edit/remove modals - Full CRUD template with stats cards, store filter, member table - 7 Pydantic schemas for merchant team request/response - 2 service methods: validate_store_ownership, get_merchant_team_members - 25 new i18n keys across 4 tenancy locales + 1 core common key Tests: 434 tenancy tests passing, arch-check green. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -118,6 +118,12 @@ from app.modules.tenancy.schemas.team import (
|
||||
InvitationAccept,
|
||||
InvitationAcceptResponse,
|
||||
InvitationResponse,
|
||||
MerchantTeamInvite,
|
||||
MerchantTeamInviteResponse,
|
||||
MerchantTeamMemberResponse,
|
||||
MerchantTeamMemberStoreInfo,
|
||||
MerchantTeamOverviewResponse,
|
||||
MerchantTeamStoreInfo,
|
||||
PermissionCheckRequest,
|
||||
PermissionCheckResponse,
|
||||
RoleBase,
|
||||
|
||||
@@ -315,3 +315,89 @@ class TeamErrorResponse(BaseModel):
|
||||
error_code: str
|
||||
message: str
|
||||
details: dict | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Merchant Team Schemas (Hub View)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class MerchantTeamMemberStoreInfo(BaseModel):
|
||||
"""A member's role/status in one specific store."""
|
||||
|
||||
store_id: int
|
||||
store_name: str
|
||||
store_code: str
|
||||
role_name: str | None = None
|
||||
role_id: int | None = None
|
||||
is_active: bool = True
|
||||
is_pending: bool = False
|
||||
|
||||
|
||||
class MerchantTeamMemberResponse(BaseModel):
|
||||
"""A team member aggregated across all merchant stores."""
|
||||
|
||||
user_id: int
|
||||
email: EmailStr
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
full_name: str
|
||||
stores: list[MerchantTeamMemberStoreInfo] = Field(default_factory=list)
|
||||
is_owner: bool = False
|
||||
|
||||
|
||||
class MerchantTeamStoreInfo(BaseModel):
|
||||
"""Compact store info for the merchant team overview."""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
code: str
|
||||
|
||||
|
||||
class MerchantTeamOverviewResponse(BaseModel):
|
||||
"""Merchant team overview with member-centric view."""
|
||||
|
||||
merchant_name: str
|
||||
stores: list[MerchantTeamStoreInfo]
|
||||
members: list[MerchantTeamMemberResponse]
|
||||
total_members: int
|
||||
total_active: int
|
||||
total_pending: int
|
||||
|
||||
|
||||
class MerchantTeamInvite(BaseModel):
|
||||
"""Schema for inviting a member to merchant stores."""
|
||||
|
||||
email: EmailStr
|
||||
first_name: str | None = Field(None, max_length=100)
|
||||
last_name: str | None = Field(None, max_length=100)
|
||||
store_ids: list[int] = Field(..., min_length=1, description="Store IDs to invite to")
|
||||
role_name: str = Field("staff", description="Role name for all selected stores")
|
||||
|
||||
@field_validator("role_name")
|
||||
@classmethod
|
||||
def validate_role_name(cls, v):
|
||||
"""Validate role name is in allowed presets."""
|
||||
allowed_roles = ["manager", "staff", "support", "viewer", "marketing"]
|
||||
if v.lower() not in allowed_roles:
|
||||
raise ValueError(
|
||||
f"Role name must be one of: {', '.join(allowed_roles)}"
|
||||
)
|
||||
return v.lower()
|
||||
|
||||
|
||||
class MerchantTeamInviteResult(BaseModel):
|
||||
"""Per-store invite result."""
|
||||
|
||||
store_id: int
|
||||
store_name: str
|
||||
success: bool
|
||||
error: str | None = None
|
||||
|
||||
|
||||
class MerchantTeamInviteResponse(BaseModel):
|
||||
"""Response for merchant team invite (multi-store)."""
|
||||
|
||||
message: str
|
||||
email: EmailStr
|
||||
results: list[MerchantTeamInviteResult]
|
||||
|
||||
Reference in New Issue
Block a user