Spaces:
Runtime error
feat(system_users): implement user status enum and migrate from boolean flag
Browse files- Add UserStatus enum with states: ACTIVE, INACTIVE, SUSPENDED, LOCKED, PENDING_ACTIVATION
- Replace is_active boolean field with status enum field in SystemUserModel
- Update CreateUserRequest and UpdateUserRequest schemas to use status field
- Update UserInfoResponse schema to reflect status field instead of is_active
- Migrate EmployeeService to use UserStatus.ACTIVE when creating system users
- Migrate MerchantService to use UserStatus.ACTIVE and remove duplicate status field
- Update system_users router documentation to reference status_filter parameter
- Rename is_active filter parameter to status_filter in list_users endpoint
- Add migration utility scripts for status field migration
- Provides better semantic clarity and extensibility for user account states
- app/employees/services/service.py +2 -1
- app/merchants/services/service.py +2 -2
- app/system_users/controllers/router.py +4 -4
- app/system_users/models/model.py +12 -2
- app/system_users/schemas/schema.py +9 -8
- app/system_users/services/service.py +24 -26
- migration_status_field.py +268 -0
- test_status_migration.py +189 -0
|
@@ -20,6 +20,7 @@ from app.employees.models.model import EmployeeModel
|
|
| 20 |
from app.employees.schemas.schema import EmployeeCreate, EmployeeUpdate, EmployeeResponse
|
| 21 |
from app.system_users.services.service import SystemUserService
|
| 22 |
from app.system_users.schemas.schema import CreateUserRequest
|
|
|
|
| 23 |
|
| 24 |
logger = get_logger(__name__)
|
| 25 |
|
|
@@ -742,7 +743,7 @@ class EmployeeService:
|
|
| 742 |
password=temp_password,
|
| 743 |
full_name=f"{employee_payload.first_name} {employee_payload.last_name or ''}".strip(),
|
| 744 |
role_id=role_id,
|
| 745 |
-
|
| 746 |
metadata={
|
| 747 |
"employee_user_id": employee_user_id,
|
| 748 |
"employee_code": employee_payload.employee_code,
|
|
|
|
| 20 |
from app.employees.schemas.schema import EmployeeCreate, EmployeeUpdate, EmployeeResponse
|
| 21 |
from app.system_users.services.service import SystemUserService
|
| 22 |
from app.system_users.schemas.schema import CreateUserRequest
|
| 23 |
+
from app.system_users.models.model import UserStatus
|
| 24 |
|
| 25 |
logger = get_logger(__name__)
|
| 26 |
|
|
|
|
| 743 |
password=temp_password,
|
| 744 |
full_name=f"{employee_payload.first_name} {employee_payload.last_name or ''}".strip(),
|
| 745 |
role_id=role_id,
|
| 746 |
+
status=UserStatus.ACTIVE,
|
| 747 |
metadata={
|
| 748 |
"employee_user_id": employee_user_id,
|
| 749 |
"employee_code": employee_payload.employee_code,
|
|
@@ -15,6 +15,7 @@ from app.merchants.models.model import MerchantModel
|
|
| 15 |
from app.merchants.schemas.schema import MerchantCreate, MerchantUpdate, MerchantResponse
|
| 16 |
from app.system_users.services.service import SystemUserService
|
| 17 |
from app.system_users.schemas.schema import CreateUserRequest
|
|
|
|
| 18 |
from pymongo.errors import DuplicateKeyError
|
| 19 |
|
| 20 |
logger = get_logger(__name__)
|
|
@@ -1143,8 +1144,7 @@ class MerchantService:
|
|
| 1143 |
full_name=merchant_payload.merchant_name,
|
| 1144 |
role_id=role_id,
|
| 1145 |
merchant_id=merchant_id,
|
| 1146 |
-
|
| 1147 |
-
status="active",
|
| 1148 |
metadata={
|
| 1149 |
"created_for_merchant": merchant_id,
|
| 1150 |
"merchant_type": merchant_payload.merchant_type.value,
|
|
|
|
| 15 |
from app.merchants.schemas.schema import MerchantCreate, MerchantUpdate, MerchantResponse
|
| 16 |
from app.system_users.services.service import SystemUserService
|
| 17 |
from app.system_users.schemas.schema import CreateUserRequest
|
| 18 |
+
from app.system_users.models.model import UserStatus
|
| 19 |
from pymongo.errors import DuplicateKeyError
|
| 20 |
|
| 21 |
logger = get_logger(__name__)
|
|
|
|
| 1144 |
full_name=merchant_payload.merchant_name,
|
| 1145 |
role_id=role_id,
|
| 1146 |
merchant_id=merchant_id,
|
| 1147 |
+
status=UserStatus.ACTIVE,
|
|
|
|
| 1148 |
metadata={
|
| 1149 |
"created_for_merchant": merchant_id,
|
| 1150 |
"merchant_type": merchant_payload.merchant_type.value,
|
|
@@ -36,12 +36,12 @@ async def list_users(
|
|
| 36 |
user_service: SystemUserService = Depends(get_system_user_service)
|
| 37 |
):
|
| 38 |
"""
|
| 39 |
-
List users with pagination and optional field projection
|
| 40 |
|
| 41 |
**Request Body:**
|
| 42 |
- `page`: Page number (default: 1)
|
| 43 |
- `page_size`: Page size (default: 20, max: 100)
|
| 44 |
-
- `
|
| 45 |
- `projection_list`: Optional list of fields to include in response
|
| 46 |
"""
|
| 47 |
try:
|
|
@@ -50,11 +50,11 @@ async def list_users(
|
|
| 50 |
users, total_count = await user_service.list_users(
|
| 51 |
payload.page,
|
| 52 |
page_size,
|
| 53 |
-
payload.
|
| 54 |
payload.projection_list
|
| 55 |
)
|
| 56 |
|
| 57 |
-
# If projection is used, return raw data
|
| 58 |
if payload.projection_list:
|
| 59 |
return {
|
| 60 |
"users": users,
|
|
|
|
| 36 |
user_service: SystemUserService = Depends(get_system_user_service)
|
| 37 |
):
|
| 38 |
"""
|
| 39 |
+
List users with pagination and optional field projection following API standards.
|
| 40 |
|
| 41 |
**Request Body:**
|
| 42 |
- `page`: Page number (default: 1)
|
| 43 |
- `page_size`: Page size (default: 20, max: 100)
|
| 44 |
+
- `status_filter`: Filter by user status (active, inactive, suspended, etc.)
|
| 45 |
- `projection_list`: Optional list of fields to include in response
|
| 46 |
"""
|
| 47 |
try:
|
|
|
|
| 50 |
users, total_count = await user_service.list_users(
|
| 51 |
payload.page,
|
| 52 |
page_size,
|
| 53 |
+
payload.status_filter,
|
| 54 |
payload.projection_list
|
| 55 |
)
|
| 56 |
|
| 57 |
+
# If projection is used, return raw data (API standard)
|
| 58 |
if payload.projection_list:
|
| 59 |
return {
|
| 60 |
"users": users,
|
|
@@ -2,6 +2,16 @@
|
|
| 2 |
from datetime import datetime
|
| 3 |
from typing import Optional, Dict, Any
|
| 4 |
from pydantic import BaseModel, Field, EmailStr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
|
| 7 |
class SystemUserModel(BaseModel):
|
|
@@ -15,7 +25,7 @@ class SystemUserModel(BaseModel):
|
|
| 15 |
role_id: Optional[str] = Field(None, description="Role identifier e.g. role_distributor_manager")
|
| 16 |
merchant_id: Optional[str] = Field(None, description="Owning merchant id")
|
| 17 |
merchant_type: Optional[str] = Field(None, description="Merchant type (national_cnf, cnf, distributor, salon)")
|
| 18 |
-
|
| 19 |
last_login: Optional[datetime] = Field(None, description="Timestamp of last successful login")
|
| 20 |
created_by: str = Field(..., description="User id who created this record")
|
| 21 |
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation timestamp")
|
|
@@ -33,7 +43,7 @@ class SystemUserModel(BaseModel):
|
|
| 33 |
"full_name": "Vikram Singh",
|
| 34 |
"role_id": "role_distributor_manager",
|
| 35 |
"merchant_id": "dist_delhi_premium",
|
| 36 |
-
"
|
| 37 |
"last_login": None,
|
| 38 |
"created_by": "user_company_admin",
|
| 39 |
"created_at": "2025-11-30T16:52:33.902Z"
|
|
|
|
| 2 |
from datetime import datetime
|
| 3 |
from typing import Optional, Dict, Any
|
| 4 |
from pydantic import BaseModel, Field, EmailStr
|
| 5 |
+
from enum import Enum
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class UserStatus(str, Enum):
|
| 9 |
+
"""User account status options."""
|
| 10 |
+
ACTIVE = "active"
|
| 11 |
+
INACTIVE = "inactive"
|
| 12 |
+
SUSPENDED = "suspended"
|
| 13 |
+
LOCKED = "locked"
|
| 14 |
+
PENDING_ACTIVATION = "pending_activation"
|
| 15 |
|
| 16 |
|
| 17 |
class SystemUserModel(BaseModel):
|
|
|
|
| 25 |
role_id: Optional[str] = Field(None, description="Role identifier e.g. role_distributor_manager")
|
| 26 |
merchant_id: Optional[str] = Field(None, description="Owning merchant id")
|
| 27 |
merchant_type: Optional[str] = Field(None, description="Merchant type (national_cnf, cnf, distributor, salon)")
|
| 28 |
+
status: UserStatus = Field(default=UserStatus.ACTIVE, description="Account status")
|
| 29 |
last_login: Optional[datetime] = Field(None, description="Timestamp of last successful login")
|
| 30 |
created_by: str = Field(..., description="User id who created this record")
|
| 31 |
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation timestamp")
|
|
|
|
| 43 |
"full_name": "Vikram Singh",
|
| 44 |
"role_id": "role_distributor_manager",
|
| 45 |
"merchant_id": "dist_delhi_premium",
|
| 46 |
+
"status": "active",
|
| 47 |
"last_login": None,
|
| 48 |
"created_by": "user_company_admin",
|
| 49 |
"created_at": "2025-11-30T16:52:33.902Z"
|
|
@@ -2,6 +2,7 @@
|
|
| 2 |
from datetime import datetime
|
| 3 |
from typing import Optional, List, Dict
|
| 4 |
from pydantic import BaseModel, Field, EmailStr, validator
|
|
|
|
| 5 |
|
| 6 |
|
| 7 |
class LoginRequest(BaseModel):
|
|
@@ -31,7 +32,7 @@ class UserInfoResponse(BaseModel):
|
|
| 31 |
role_id: Optional[str] = Field(None, description="Role identifier")
|
| 32 |
merchant_id: Optional[str] = Field(None, description="Merchant identifier")
|
| 33 |
merchant_type: Optional[str] = Field(None, description="Merchant type")
|
| 34 |
-
|
| 35 |
last_login: Optional[datetime] = Field(None, description="Last login timestamp")
|
| 36 |
created_by: str = Field(..., description="Creator user id")
|
| 37 |
created_at: datetime = Field(..., description="Creation timestamp")
|
|
@@ -47,7 +48,7 @@ class CreateUserRequest(BaseModel):
|
|
| 47 |
password: str = Field(..., description="Password", min_length=8, max_length=100)
|
| 48 |
full_name: str = Field(..., description="Full name", min_length=1, max_length=100)
|
| 49 |
role_id: str = Field(..., description="Role identifier")
|
| 50 |
-
|
| 51 |
metadata: Optional[Dict[str, str]] = Field(None, description="Optional metadata")
|
| 52 |
|
| 53 |
@validator("username")
|
|
@@ -78,7 +79,7 @@ class UpdateUserRequest(BaseModel):
|
|
| 78 |
role_id: Optional[str] = Field(None, description="Role identifier")
|
| 79 |
merchant_id: Optional[str] = Field(None, description="Merchant identifier")
|
| 80 |
merchant_type: Optional[str] = Field(None, description="Merchant type (national_cnf, cnf, distributor, salon)")
|
| 81 |
-
|
| 82 |
metadata: Optional[Dict[str, str]] = Field(None, description="Optional metadata")
|
| 83 |
|
| 84 |
@validator("username")
|
|
@@ -148,19 +149,19 @@ LoginResponse.model_rebuild()
|
|
| 148 |
|
| 149 |
|
| 150 |
class SystemUserListRequest(BaseModel):
|
| 151 |
-
"""Schema for listing system users with filters (POST request body)."""
|
| 152 |
page: int = Field(1, ge=1, description="Page number (default: 1)")
|
| 153 |
page_size: int = Field(20, ge=1, le=100, description="Page size (default: 20, max: 100)")
|
| 154 |
-
|
| 155 |
-
projection_list: Optional[List[str]] = Field(None, description="List of fields to include in response
|
| 156 |
|
| 157 |
class Config:
|
| 158 |
json_schema_extra = {
|
| 159 |
"example": {
|
| 160 |
"page": 1,
|
| 161 |
"page_size": 20,
|
| 162 |
-
"
|
| 163 |
-
"projection_list": ["user_id", "username", "email", "full_name", "role_id"]
|
| 164 |
}
|
| 165 |
}
|
| 166 |
|
|
|
|
| 2 |
from datetime import datetime
|
| 3 |
from typing import Optional, List, Dict
|
| 4 |
from pydantic import BaseModel, Field, EmailStr, validator
|
| 5 |
+
from app.system_users.models.model import UserStatus
|
| 6 |
|
| 7 |
|
| 8 |
class LoginRequest(BaseModel):
|
|
|
|
| 32 |
role_id: Optional[str] = Field(None, description="Role identifier")
|
| 33 |
merchant_id: Optional[str] = Field(None, description="Merchant identifier")
|
| 34 |
merchant_type: Optional[str] = Field(None, description="Merchant type")
|
| 35 |
+
status: UserStatus = Field(..., description="Account status")
|
| 36 |
last_login: Optional[datetime] = Field(None, description="Last login timestamp")
|
| 37 |
created_by: str = Field(..., description="Creator user id")
|
| 38 |
created_at: datetime = Field(..., description="Creation timestamp")
|
|
|
|
| 48 |
password: str = Field(..., description="Password", min_length=8, max_length=100)
|
| 49 |
full_name: str = Field(..., description="Full name", min_length=1, max_length=100)
|
| 50 |
role_id: str = Field(..., description="Role identifier")
|
| 51 |
+
status: UserStatus = Field(default=UserStatus.ACTIVE, description="Account status")
|
| 52 |
metadata: Optional[Dict[str, str]] = Field(None, description="Optional metadata")
|
| 53 |
|
| 54 |
@validator("username")
|
|
|
|
| 79 |
role_id: Optional[str] = Field(None, description="Role identifier")
|
| 80 |
merchant_id: Optional[str] = Field(None, description="Merchant identifier")
|
| 81 |
merchant_type: Optional[str] = Field(None, description="Merchant type (national_cnf, cnf, distributor, salon)")
|
| 82 |
+
status: Optional[UserStatus] = Field(None, description="Account status")
|
| 83 |
metadata: Optional[Dict[str, str]] = Field(None, description="Optional metadata")
|
| 84 |
|
| 85 |
@validator("username")
|
|
|
|
| 149 |
|
| 150 |
|
| 151 |
class SystemUserListRequest(BaseModel):
|
| 152 |
+
"""Schema for listing system users with filters (POST request body) following API standards."""
|
| 153 |
page: int = Field(1, ge=1, description="Page number (default: 1)")
|
| 154 |
page_size: int = Field(20, ge=1, le=100, description="Page size (default: 20, max: 100)")
|
| 155 |
+
status_filter: Optional[UserStatus] = Field(None, description="Filter by user status")
|
| 156 |
+
projection_list: Optional[List[str]] = Field(None, description="List of fields to include in response")
|
| 157 |
|
| 158 |
class Config:
|
| 159 |
json_schema_extra = {
|
| 160 |
"example": {
|
| 161 |
"page": 1,
|
| 162 |
"page_size": 20,
|
| 163 |
+
"status_filter": "active",
|
| 164 |
+
"projection_list": ["user_id", "username", "email", "full_name", "role_id", "status"]
|
| 165 |
}
|
| 166 |
}
|
| 167 |
|
|
@@ -10,7 +10,7 @@ from jose import JWTError, jwt
|
|
| 10 |
from fastapi import HTTPException, status
|
| 11 |
from insightfy_utils.logging import get_logger
|
| 12 |
|
| 13 |
-
from app.system_users.models.model import SystemUserModel
|
| 14 |
from app.system_users.schemas.schema import (
|
| 15 |
CreateUserRequest,
|
| 16 |
UpdateUserRequest,
|
|
@@ -149,7 +149,7 @@ class SystemUserService:
|
|
| 149 |
role_id=user_data.role_id,
|
| 150 |
merchant_id=user_data.merchant_id,
|
| 151 |
merchant_type=user_data.merchant_type,
|
| 152 |
-
|
| 153 |
last_login=None,
|
| 154 |
created_by=created_by,
|
| 155 |
created_at=datetime.utcnow(),
|
|
@@ -181,8 +181,9 @@ class SystemUserService:
|
|
| 181 |
logger.warning(f"Login attempt with non-existent identifier: {identifier}")
|
| 182 |
return None, "Invalid email or username"
|
| 183 |
|
| 184 |
-
|
| 185 |
-
|
|
|
|
| 186 |
|
| 187 |
if not self.verify_password(password, user.password_hash):
|
| 188 |
return None, "Invalid username or password"
|
|
@@ -261,18 +262,18 @@ class SystemUserService:
|
|
| 261 |
logger.error(f"Error changing password for user {user_id}: {e}")
|
| 262 |
return False
|
| 263 |
|
| 264 |
-
async def list_users(self, page: int = 1, page_size: int = 20,
|
| 265 |
-
"""List users with pagination, optional
|
| 266 |
try:
|
| 267 |
skip = (page - 1) * page_size
|
| 268 |
|
| 269 |
# Build query filter
|
| 270 |
query_filter = {}
|
| 271 |
-
if
|
| 272 |
-
query_filter["
|
| 273 |
|
| 274 |
# Debug logging
|
| 275 |
-
logger.info(f"list_users called: page={page}, page_size={page_size},
|
| 276 |
logger.info(f"Database: {self.db.name}, Collection: {self.collection.name}")
|
| 277 |
logger.info(f"Query filter: {query_filter}")
|
| 278 |
|
|
@@ -280,24 +281,21 @@ class SystemUserService:
|
|
| 280 |
total_count = await self.collection.count_documents(query_filter)
|
| 281 |
logger.info(f"Total count: {total_count}")
|
| 282 |
|
| 283 |
-
# Build projection
|
| 284 |
projection_dict = None
|
| 285 |
if projection_list:
|
| 286 |
-
# Create projection with specified fields
|
| 287 |
projection_dict = {field: 1 for field in projection_list}
|
| 288 |
-
#
|
| 289 |
-
if "_id" not in projection_list:
|
| 290 |
-
projection_dict["_id"] = 0
|
| 291 |
|
| 292 |
-
#
|
| 293 |
cursor = self.collection.find(query_filter, projection_dict).sort("created_at", -1).skip(skip).limit(page_size)
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
|
| 302 |
logger.info(f"Returning {len(users)} users")
|
| 303 |
return users, total_count
|
|
@@ -312,7 +310,7 @@ class SystemUserService:
|
|
| 312 |
result = await self.collection.update_one(
|
| 313 |
{"user_id": user_id},
|
| 314 |
{"$set": {
|
| 315 |
-
"
|
| 316 |
"updated_at": datetime.utcnow(),
|
| 317 |
"updated_by": deactivated_by
|
| 318 |
}}
|
|
@@ -333,7 +331,7 @@ class SystemUserService:
|
|
| 333 |
result = await self.collection.update_one(
|
| 334 |
{"user_id": user_id},
|
| 335 |
{"$set": {
|
| 336 |
-
"
|
| 337 |
"suspension_reason": reason,
|
| 338 |
"suspended_at": datetime.utcnow(),
|
| 339 |
"suspended_by": suspended_by,
|
|
@@ -397,7 +395,7 @@ class SystemUserService:
|
|
| 397 |
{"user_id": user_id},
|
| 398 |
{
|
| 399 |
"$set": {
|
| 400 |
-
"
|
| 401 |
"updated_at": datetime.utcnow(),
|
| 402 |
"updated_by": unlocked_by
|
| 403 |
},
|
|
@@ -430,7 +428,7 @@ class SystemUserService:
|
|
| 430 |
role_id=user.role_id,
|
| 431 |
merchant_id=user.merchant_id,
|
| 432 |
merchant_type=user.merchant_type,
|
| 433 |
-
|
| 434 |
last_login=user.last_login,
|
| 435 |
created_by=user.created_by,
|
| 436 |
created_at=user.created_at,
|
|
|
|
| 10 |
from fastapi import HTTPException, status
|
| 11 |
from insightfy_utils.logging import get_logger
|
| 12 |
|
| 13 |
+
from app.system_users.models.model import SystemUserModel, UserStatus
|
| 14 |
from app.system_users.schemas.schema import (
|
| 15 |
CreateUserRequest,
|
| 16 |
UpdateUserRequest,
|
|
|
|
| 149 |
role_id=user_data.role_id,
|
| 150 |
merchant_id=user_data.merchant_id,
|
| 151 |
merchant_type=user_data.merchant_type,
|
| 152 |
+
status=UserStatus.ACTIVE, # Set as active by default
|
| 153 |
last_login=None,
|
| 154 |
created_by=created_by,
|
| 155 |
created_at=datetime.utcnow(),
|
|
|
|
| 181 |
logger.warning(f"Login attempt with non-existent identifier: {identifier}")
|
| 182 |
return None, "Invalid email or username"
|
| 183 |
|
| 184 |
+
# Check account status
|
| 185 |
+
if user.status not in [UserStatus.ACTIVE]:
|
| 186 |
+
return None, f"Account is {user.status.value}"
|
| 187 |
|
| 188 |
if not self.verify_password(password, user.password_hash):
|
| 189 |
return None, "Invalid username or password"
|
|
|
|
| 262 |
logger.error(f"Error changing password for user {user_id}: {e}")
|
| 263 |
return False
|
| 264 |
|
| 265 |
+
async def list_users(self, page: int = 1, page_size: int = 20, status_filter: Optional[UserStatus] = None, projection_list: Optional[List[str]] = None):
|
| 266 |
+
"""List users with pagination, optional status filter, and field projection following API standards."""
|
| 267 |
try:
|
| 268 |
skip = (page - 1) * page_size
|
| 269 |
|
| 270 |
# Build query filter
|
| 271 |
query_filter = {}
|
| 272 |
+
if status_filter is not None:
|
| 273 |
+
query_filter["status"] = status_filter.value
|
| 274 |
|
| 275 |
# Debug logging
|
| 276 |
+
logger.info(f"list_users called: page={page}, page_size={page_size}, status_filter={status_filter}, projection_list={projection_list}")
|
| 277 |
logger.info(f"Database: {self.db.name}, Collection: {self.collection.name}")
|
| 278 |
logger.info(f"Query filter: {query_filter}")
|
| 279 |
|
|
|
|
| 281 |
total_count = await self.collection.count_documents(query_filter)
|
| 282 |
logger.info(f"Total count: {total_count}")
|
| 283 |
|
| 284 |
+
# Build MongoDB projection following API standards
|
| 285 |
projection_dict = None
|
| 286 |
if projection_list:
|
|
|
|
| 287 |
projection_dict = {field: 1 for field in projection_list}
|
| 288 |
+
projection_dict["_id"] = 0 # Always exclude _id for projection
|
|
|
|
|
|
|
| 289 |
|
| 290 |
+
# Query with projection
|
| 291 |
cursor = self.collection.find(query_filter, projection_dict).sort("created_at", -1).skip(skip).limit(page_size)
|
| 292 |
+
docs = await cursor.to_list(length=page_size)
|
| 293 |
+
|
| 294 |
+
# Return raw dict if projection, model otherwise (API standard)
|
| 295 |
+
if projection_list:
|
| 296 |
+
users = docs
|
| 297 |
+
else:
|
| 298 |
+
users = [SystemUserModel(**doc) for doc in docs]
|
| 299 |
|
| 300 |
logger.info(f"Returning {len(users)} users")
|
| 301 |
return users, total_count
|
|
|
|
| 310 |
result = await self.collection.update_one(
|
| 311 |
{"user_id": user_id},
|
| 312 |
{"$set": {
|
| 313 |
+
"status": UserStatus.INACTIVE.value,
|
| 314 |
"updated_at": datetime.utcnow(),
|
| 315 |
"updated_by": deactivated_by
|
| 316 |
}}
|
|
|
|
| 331 |
result = await self.collection.update_one(
|
| 332 |
{"user_id": user_id},
|
| 333 |
{"$set": {
|
| 334 |
+
"status": UserStatus.SUSPENDED.value,
|
| 335 |
"suspension_reason": reason,
|
| 336 |
"suspended_at": datetime.utcnow(),
|
| 337 |
"suspended_by": suspended_by,
|
|
|
|
| 395 |
{"user_id": user_id},
|
| 396 |
{
|
| 397 |
"$set": {
|
| 398 |
+
"status": UserStatus.ACTIVE.value,
|
| 399 |
"updated_at": datetime.utcnow(),
|
| 400 |
"updated_by": unlocked_by
|
| 401 |
},
|
|
|
|
| 428 |
role_id=user.role_id,
|
| 429 |
merchant_id=user.merchant_id,
|
| 430 |
merchant_type=user.merchant_type,
|
| 431 |
+
status=user.status,
|
| 432 |
last_login=user.last_login,
|
| 433 |
created_by=user.created_by,
|
| 434 |
created_at=user.created_at,
|
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Migration script to convert is_active field to status field in system users.
|
| 4 |
+
|
| 5 |
+
This script:
|
| 6 |
+
1. Connects to MongoDB
|
| 7 |
+
2. Finds all documents with is_active field
|
| 8 |
+
3. Converts is_active boolean to status enum
|
| 9 |
+
4. Updates documents with new status field
|
| 10 |
+
5. Optionally removes is_active field after migration
|
| 11 |
+
|
| 12 |
+
Usage:
|
| 13 |
+
python migration_status_field.py --dry-run # Preview changes
|
| 14 |
+
python migration_status_field.py --execute # Execute migration
|
| 15 |
+
python migration_status_field.py --cleanup # Remove is_active field after migration
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import asyncio
|
| 19 |
+
import argparse
|
| 20 |
+
import logging
|
| 21 |
+
from datetime import datetime
|
| 22 |
+
from motor.motor_asyncio import AsyncIOMotorClient
|
| 23 |
+
from app.core.config import settings
|
| 24 |
+
from app.system_users.constants import SCM_SYSTEM_USERS_COLLECTION
|
| 25 |
+
|
| 26 |
+
# Configure logging
|
| 27 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 28 |
+
logger = logging.getLogger(__name__)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class StatusFieldMigration:
|
| 32 |
+
"""Migration class for converting is_active to status field."""
|
| 33 |
+
|
| 34 |
+
def __init__(self):
|
| 35 |
+
self.client = None
|
| 36 |
+
self.db = None
|
| 37 |
+
self.collection = None
|
| 38 |
+
|
| 39 |
+
async def connect(self):
|
| 40 |
+
"""Connect to MongoDB."""
|
| 41 |
+
try:
|
| 42 |
+
self.client = AsyncIOMotorClient(settings.MONGODB_URI)
|
| 43 |
+
self.db = self.client[settings.MONGODB_DB_NAME]
|
| 44 |
+
self.collection = self.db[SCM_SYSTEM_USERS_COLLECTION]
|
| 45 |
+
|
| 46 |
+
# Test connection
|
| 47 |
+
await self.client.admin.command('ping')
|
| 48 |
+
logger.info("β
Connected to MongoDB successfully")
|
| 49 |
+
|
| 50 |
+
except Exception as e:
|
| 51 |
+
logger.error(f"β Failed to connect to MongoDB: {e}")
|
| 52 |
+
raise
|
| 53 |
+
|
| 54 |
+
async def disconnect(self):
|
| 55 |
+
"""Disconnect from MongoDB."""
|
| 56 |
+
if self.client:
|
| 57 |
+
self.client.close()
|
| 58 |
+
logger.info("π Disconnected from MongoDB")
|
| 59 |
+
|
| 60 |
+
async def analyze_data(self):
|
| 61 |
+
"""Analyze current data state."""
|
| 62 |
+
logger.info("π Analyzing current data state...")
|
| 63 |
+
|
| 64 |
+
# Count documents with is_active field
|
| 65 |
+
has_is_active = await self.collection.count_documents({"is_active": {"$exists": True}})
|
| 66 |
+
|
| 67 |
+
# Count documents with status field
|
| 68 |
+
has_status = await self.collection.count_documents({"status": {"$exists": True}})
|
| 69 |
+
|
| 70 |
+
# Count active/inactive users
|
| 71 |
+
active_users = await self.collection.count_documents({"is_active": True})
|
| 72 |
+
inactive_users = await self.collection.count_documents({"is_active": False})
|
| 73 |
+
|
| 74 |
+
# Count by status values
|
| 75 |
+
status_counts = {}
|
| 76 |
+
async for doc in self.collection.aggregate([
|
| 77 |
+
{"$match": {"status": {"$exists": True}}},
|
| 78 |
+
{"$group": {"_id": "$status", "count": {"$sum": 1}}}
|
| 79 |
+
]):
|
| 80 |
+
status_counts[doc["_id"]] = doc["count"]
|
| 81 |
+
|
| 82 |
+
logger.info(f"π Analysis Results:")
|
| 83 |
+
logger.info(f" Documents with is_active field: {has_is_active}")
|
| 84 |
+
logger.info(f" Documents with status field: {has_status}")
|
| 85 |
+
logger.info(f" Active users (is_active=true): {active_users}")
|
| 86 |
+
logger.info(f" Inactive users (is_active=false): {inactive_users}")
|
| 87 |
+
|
| 88 |
+
if status_counts:
|
| 89 |
+
logger.info(f" Status field distribution:")
|
| 90 |
+
for status, count in status_counts.items():
|
| 91 |
+
logger.info(f" {status}: {count}")
|
| 92 |
+
|
| 93 |
+
return {
|
| 94 |
+
"has_is_active": has_is_active,
|
| 95 |
+
"has_status": has_status,
|
| 96 |
+
"active_users": active_users,
|
| 97 |
+
"inactive_users": inactive_users,
|
| 98 |
+
"status_counts": status_counts
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
async def preview_migration(self):
|
| 102 |
+
"""Preview what the migration would do."""
|
| 103 |
+
logger.info("π Previewing migration changes...")
|
| 104 |
+
|
| 105 |
+
# Find documents that need migration
|
| 106 |
+
cursor = self.collection.find(
|
| 107 |
+
{"is_active": {"$exists": True}, "status": {"$exists": False}},
|
| 108 |
+
{"user_id": 1, "username": 1, "is_active": 1}
|
| 109 |
+
).limit(10)
|
| 110 |
+
|
| 111 |
+
preview_count = 0
|
| 112 |
+
async for doc in cursor:
|
| 113 |
+
new_status = "active" if doc.get("is_active", True) else "inactive"
|
| 114 |
+
logger.info(f" {doc.get('user_id', 'N/A')} ({doc.get('username', 'N/A')}): is_active={doc.get('is_active')} β status='{new_status}'")
|
| 115 |
+
preview_count += 1
|
| 116 |
+
|
| 117 |
+
total_to_migrate = await self.collection.count_documents(
|
| 118 |
+
{"is_active": {"$exists": True}, "status": {"$exists": False}}
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
if preview_count < total_to_migrate:
|
| 122 |
+
logger.info(f" ... and {total_to_migrate - preview_count} more documents")
|
| 123 |
+
|
| 124 |
+
logger.info(f"π Total documents to migrate: {total_to_migrate}")
|
| 125 |
+
return total_to_migrate
|
| 126 |
+
|
| 127 |
+
async def execute_migration(self):
|
| 128 |
+
"""Execute the migration."""
|
| 129 |
+
logger.info("π Executing migration...")
|
| 130 |
+
|
| 131 |
+
# Migration logic: is_active=true β status="active", is_active=false β status="inactive"
|
| 132 |
+
migration_time = datetime.utcnow()
|
| 133 |
+
|
| 134 |
+
# Update active users
|
| 135 |
+
result_active = await self.collection.update_many(
|
| 136 |
+
{"is_active": True, "status": {"$exists": False}},
|
| 137 |
+
{
|
| 138 |
+
"$set": {
|
| 139 |
+
"status": "active",
|
| 140 |
+
"migrated_at": migration_time,
|
| 141 |
+
"migration_note": "Migrated from is_active=true"
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
# Update inactive users
|
| 147 |
+
result_inactive = await self.collection.update_many(
|
| 148 |
+
{"is_active": False, "status": {"$exists": False}},
|
| 149 |
+
{
|
| 150 |
+
"$set": {
|
| 151 |
+
"status": "inactive",
|
| 152 |
+
"migrated_at": migration_time,
|
| 153 |
+
"migration_note": "Migrated from is_active=false"
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
logger.info(f"β
Migration completed:")
|
| 159 |
+
logger.info(f" Active users migrated: {result_active.modified_count}")
|
| 160 |
+
logger.info(f" Inactive users migrated: {result_inactive.modified_count}")
|
| 161 |
+
logger.info(f" Total documents updated: {result_active.modified_count + result_inactive.modified_count}")
|
| 162 |
+
|
| 163 |
+
return result_active.modified_count + result_inactive.modified_count
|
| 164 |
+
|
| 165 |
+
async def cleanup_is_active_field(self):
|
| 166 |
+
"""Remove is_active field after successful migration."""
|
| 167 |
+
logger.info("π§Ή Cleaning up is_active field...")
|
| 168 |
+
|
| 169 |
+
# Only remove is_active if status field exists (safety check)
|
| 170 |
+
result = await self.collection.update_many(
|
| 171 |
+
{"is_active": {"$exists": True}, "status": {"$exists": True}},
|
| 172 |
+
{"$unset": {"is_active": ""}}
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
logger.info(f"β
Cleanup completed: {result.modified_count} documents updated")
|
| 176 |
+
return result.modified_count
|
| 177 |
+
|
| 178 |
+
async def verify_migration(self):
|
| 179 |
+
"""Verify migration was successful."""
|
| 180 |
+
logger.info("π Verifying migration...")
|
| 181 |
+
|
| 182 |
+
analysis = await self.analyze_data()
|
| 183 |
+
|
| 184 |
+
# Check if any documents still have is_active but no status
|
| 185 |
+
orphaned = await self.collection.count_documents(
|
| 186 |
+
{"is_active": {"$exists": True}, "status": {"$exists": False}}
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
if orphaned > 0:
|
| 190 |
+
logger.warning(f"β οΈ Found {orphaned} documents with is_active but no status field")
|
| 191 |
+
return False
|
| 192 |
+
|
| 193 |
+
logger.info("β
Migration verification passed")
|
| 194 |
+
return True
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
async def main():
|
| 198 |
+
"""Main migration function."""
|
| 199 |
+
parser = argparse.ArgumentParser(description="Migrate is_active field to status field")
|
| 200 |
+
parser.add_argument("--dry-run", action="store_true", help="Preview changes without executing")
|
| 201 |
+
parser.add_argument("--execute", action="store_true", help="Execute the migration")
|
| 202 |
+
parser.add_argument("--cleanup", action="store_true", help="Remove is_active field after migration")
|
| 203 |
+
parser.add_argument("--analyze", action="store_true", help="Analyze current data state")
|
| 204 |
+
|
| 205 |
+
args = parser.parse_args()
|
| 206 |
+
|
| 207 |
+
if not any([args.dry_run, args.execute, args.cleanup, args.analyze]):
|
| 208 |
+
parser.print_help()
|
| 209 |
+
return
|
| 210 |
+
|
| 211 |
+
migration = StatusFieldMigration()
|
| 212 |
+
|
| 213 |
+
try:
|
| 214 |
+
await migration.connect()
|
| 215 |
+
|
| 216 |
+
if args.analyze or args.dry_run:
|
| 217 |
+
await migration.analyze_data()
|
| 218 |
+
|
| 219 |
+
if args.dry_run:
|
| 220 |
+
await migration.preview_migration()
|
| 221 |
+
logger.info("π Dry run completed. Use --execute to perform the migration.")
|
| 222 |
+
|
| 223 |
+
if args.execute:
|
| 224 |
+
# Analyze first
|
| 225 |
+
await migration.analyze_data()
|
| 226 |
+
|
| 227 |
+
# Preview
|
| 228 |
+
total_to_migrate = await migration.preview_migration()
|
| 229 |
+
|
| 230 |
+
if total_to_migrate == 0:
|
| 231 |
+
logger.info("βΉοΈ No documents need migration.")
|
| 232 |
+
return
|
| 233 |
+
|
| 234 |
+
# Confirm execution
|
| 235 |
+
print(f"\nβ οΈ About to migrate {total_to_migrate} documents.")
|
| 236 |
+
confirm = input("Continue? (yes/no): ").lower().strip()
|
| 237 |
+
|
| 238 |
+
if confirm != "yes":
|
| 239 |
+
logger.info("β Migration cancelled by user")
|
| 240 |
+
return
|
| 241 |
+
|
| 242 |
+
# Execute migration
|
| 243 |
+
migrated_count = await migration.execute_migration()
|
| 244 |
+
|
| 245 |
+
# Verify
|
| 246 |
+
if await migration.verify_migration():
|
| 247 |
+
logger.info("π Migration completed successfully!")
|
| 248 |
+
else:
|
| 249 |
+
logger.error("β Migration verification failed!")
|
| 250 |
+
|
| 251 |
+
if args.cleanup:
|
| 252 |
+
# Verify migration first
|
| 253 |
+
if await migration.verify_migration():
|
| 254 |
+
cleanup_count = await migration.cleanup_is_active_field()
|
| 255 |
+
logger.info(f"π Cleanup completed! Removed is_active field from {cleanup_count} documents.")
|
| 256 |
+
else:
|
| 257 |
+
logger.error("β Cannot cleanup: Migration verification failed!")
|
| 258 |
+
|
| 259 |
+
except Exception as e:
|
| 260 |
+
logger.error(f"β Migration failed: {e}")
|
| 261 |
+
raise
|
| 262 |
+
|
| 263 |
+
finally:
|
| 264 |
+
await migration.disconnect()
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
if __name__ == "__main__":
|
| 268 |
+
asyncio.run(main())
|
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Test script to verify status field migration works correctly.
|
| 4 |
+
Tests the new UserStatus enum and service methods.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import asyncio
|
| 8 |
+
import sys
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from app.system_users.models.model import SystemUserModel, UserStatus
|
| 11 |
+
from app.system_users.schemas.schema import CreateUserRequest, SystemUserListRequest
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def test_user_status_enum():
|
| 15 |
+
"""Test UserStatus enum values."""
|
| 16 |
+
print("π§ͺ Testing UserStatus enum...")
|
| 17 |
+
|
| 18 |
+
# Test enum values
|
| 19 |
+
assert UserStatus.ACTIVE == "active"
|
| 20 |
+
assert UserStatus.INACTIVE == "inactive"
|
| 21 |
+
assert UserStatus.SUSPENDED == "suspended"
|
| 22 |
+
assert UserStatus.LOCKED == "locked"
|
| 23 |
+
assert UserStatus.PENDING_ACTIVATION == "pending_activation"
|
| 24 |
+
|
| 25 |
+
print("β
UserStatus enum test passed")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def test_system_user_model():
|
| 29 |
+
"""Test SystemUserModel with status field."""
|
| 30 |
+
print("π§ͺ Testing SystemUserModel with status field...")
|
| 31 |
+
|
| 32 |
+
# Test model creation with status
|
| 33 |
+
user_data = {
|
| 34 |
+
"user_id": "test_user_123",
|
| 35 |
+
"username": "testuser",
|
| 36 |
+
"email": "test@example.com",
|
| 37 |
+
"password_hash": "hashed_password",
|
| 38 |
+
"full_name": "Test User",
|
| 39 |
+
"role_id": "role_test",
|
| 40 |
+
"merchant_id": "merchant_123",
|
| 41 |
+
"merchant_type": "salon",
|
| 42 |
+
"status": UserStatus.ACTIVE,
|
| 43 |
+
"created_by": "admin",
|
| 44 |
+
"created_at": datetime.utcnow()
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
user = SystemUserModel(**user_data)
|
| 48 |
+
|
| 49 |
+
# Verify status field
|
| 50 |
+
assert user.status == UserStatus.ACTIVE
|
| 51 |
+
assert user.status.value == "active"
|
| 52 |
+
|
| 53 |
+
# Test different status values
|
| 54 |
+
user_data["status"] = UserStatus.SUSPENDED
|
| 55 |
+
user_suspended = SystemUserModel(**user_data)
|
| 56 |
+
assert user_suspended.status == UserStatus.SUSPENDED
|
| 57 |
+
|
| 58 |
+
print("β
SystemUserModel test passed")
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def test_create_user_request():
|
| 62 |
+
"""Test CreateUserRequest schema with status field."""
|
| 63 |
+
print("π§ͺ Testing CreateUserRequest schema...")
|
| 64 |
+
|
| 65 |
+
# Test with default status
|
| 66 |
+
request_data = {
|
| 67 |
+
"username": "newuser",
|
| 68 |
+
"email": "newuser@example.com",
|
| 69 |
+
"merchant_id": "merchant_456",
|
| 70 |
+
"password": "SecurePass123",
|
| 71 |
+
"full_name": "New User",
|
| 72 |
+
"role_id": "role_user"
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
request = CreateUserRequest(**request_data)
|
| 76 |
+
assert request.status == UserStatus.ACTIVE # Default value
|
| 77 |
+
|
| 78 |
+
# Test with explicit status
|
| 79 |
+
request_data["status"] = UserStatus.PENDING_ACTIVATION
|
| 80 |
+
request_pending = CreateUserRequest(**request_data)
|
| 81 |
+
assert request_pending.status == UserStatus.PENDING_ACTIVATION
|
| 82 |
+
|
| 83 |
+
print("β
CreateUserRequest test passed")
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def test_system_user_list_request():
|
| 87 |
+
"""Test SystemUserListRequest with status_filter."""
|
| 88 |
+
print("π§ͺ Testing SystemUserListRequest schema...")
|
| 89 |
+
|
| 90 |
+
# Test with status filter
|
| 91 |
+
list_request_data = {
|
| 92 |
+
"page": 1,
|
| 93 |
+
"page_size": 20,
|
| 94 |
+
"status_filter": UserStatus.ACTIVE,
|
| 95 |
+
"projection_list": ["user_id", "username", "email", "status"]
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
list_request = SystemUserListRequest(**list_request_data)
|
| 99 |
+
assert list_request.status_filter == UserStatus.ACTIVE
|
| 100 |
+
assert list_request.projection_list == ["user_id", "username", "email", "status"]
|
| 101 |
+
|
| 102 |
+
# Test without status filter
|
| 103 |
+
minimal_request = SystemUserListRequest(page=1, page_size=10)
|
| 104 |
+
assert minimal_request.status_filter is None
|
| 105 |
+
assert minimal_request.projection_list is None
|
| 106 |
+
|
| 107 |
+
print("β
SystemUserListRequest test passed")
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def test_authentication_logic():
|
| 111 |
+
"""Test authentication logic with status field."""
|
| 112 |
+
print("π§ͺ Testing authentication logic...")
|
| 113 |
+
|
| 114 |
+
# Simulate authentication check
|
| 115 |
+
def check_user_status(status: UserStatus) -> bool:
|
| 116 |
+
"""Simulate the authentication status check."""
|
| 117 |
+
return status in [UserStatus.ACTIVE]
|
| 118 |
+
|
| 119 |
+
# Test active user
|
| 120 |
+
assert check_user_status(UserStatus.ACTIVE) == True
|
| 121 |
+
|
| 122 |
+
# Test inactive statuses
|
| 123 |
+
assert check_user_status(UserStatus.INACTIVE) == False
|
| 124 |
+
assert check_user_status(UserStatus.SUSPENDED) == False
|
| 125 |
+
assert check_user_status(UserStatus.LOCKED) == False
|
| 126 |
+
assert check_user_status(UserStatus.PENDING_ACTIVATION) == False
|
| 127 |
+
|
| 128 |
+
print("β
Authentication logic test passed")
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def test_projection_list_example():
|
| 132 |
+
"""Test projection list functionality example."""
|
| 133 |
+
print("π§ͺ Testing projection list example...")
|
| 134 |
+
|
| 135 |
+
# Simulate MongoDB projection logic
|
| 136 |
+
def build_projection(projection_list):
|
| 137 |
+
"""Simulate the projection building logic."""
|
| 138 |
+
if not projection_list:
|
| 139 |
+
return None
|
| 140 |
+
|
| 141 |
+
projection_dict = {field: 1 for field in projection_list}
|
| 142 |
+
projection_dict["_id"] = 0 # Always exclude _id
|
| 143 |
+
return projection_dict
|
| 144 |
+
|
| 145 |
+
# Test with projection list
|
| 146 |
+
fields = ["user_id", "username", "email", "status"]
|
| 147 |
+
projection = build_projection(fields)
|
| 148 |
+
expected = {"user_id": 1, "username": 1, "email": 1, "status": 1, "_id": 0}
|
| 149 |
+
assert projection == expected
|
| 150 |
+
|
| 151 |
+
# Test without projection list
|
| 152 |
+
no_projection = build_projection(None)
|
| 153 |
+
assert no_projection is None
|
| 154 |
+
|
| 155 |
+
print("β
Projection list test passed")
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def main():
|
| 159 |
+
"""Run all tests."""
|
| 160 |
+
print("π Starting status field migration tests...\n")
|
| 161 |
+
|
| 162 |
+
try:
|
| 163 |
+
test_user_status_enum()
|
| 164 |
+
test_system_user_model()
|
| 165 |
+
test_create_user_request()
|
| 166 |
+
test_system_user_list_request()
|
| 167 |
+
test_authentication_logic()
|
| 168 |
+
test_projection_list_example()
|
| 169 |
+
|
| 170 |
+
print("\nπ All tests passed! Status field migration is working correctly.")
|
| 171 |
+
print("\nπ Migration Summary:")
|
| 172 |
+
print(" β
UserStatus enum working")
|
| 173 |
+
print(" β
SystemUserModel using status field")
|
| 174 |
+
print(" β
CreateUserRequest with status support")
|
| 175 |
+
print(" β
SystemUserListRequest with status_filter")
|
| 176 |
+
print(" β
Authentication logic updated")
|
| 177 |
+
print(" β
Projection list support implemented")
|
| 178 |
+
|
| 179 |
+
return 0
|
| 180 |
+
|
| 181 |
+
except Exception as e:
|
| 182 |
+
print(f"\nβ Test failed: {e}")
|
| 183 |
+
import traceback
|
| 184 |
+
traceback.print_exc()
|
| 185 |
+
return 1
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
if __name__ == "__main__":
|
| 189 |
+
sys.exit(main())
|