150 lines
5 KiB
Python
150 lines
5 KiB
Python
import uuid
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from ..models.account import Account
|
|
from ..models.learnable_language import LearnableLanguage
|
|
from ...auth import hash_password
|
|
from ...outbound.postgres.entities.user_entity import User as UserEntity
|
|
from ...outbound.postgres.repositories import learnable_language_repository, user_repository
|
|
|
|
|
|
class AccountService:
|
|
"""Handles account-level operations: registration, profile retrieval, and managing
|
|
the set of languages a user is learning.
|
|
|
|
All methods operate on behalf of a single authenticated user (or, for
|
|
``create_account``, the user being created).
|
|
|
|
Usage::
|
|
|
|
service = AccountService(db)
|
|
|
|
# Registration
|
|
account = await service.create_account("alice@example.com", "s3cr3t")
|
|
|
|
# Profile retrieval
|
|
account = await service.get_account(user_id)
|
|
print(account.learnable_languages) # [LearnableLanguage(...), ...]
|
|
|
|
# Add French (B1) to the account
|
|
lang = await service.add_learnable_language(
|
|
user_id, source_language="en", target_language="fr", proficiencies=["B1"]
|
|
)
|
|
|
|
# Remove it again
|
|
await service.remove_learnable_language(user_id, lang.id)
|
|
"""
|
|
|
|
def __init__(self, db: AsyncSession) -> None:
|
|
self.db = db
|
|
|
|
async def create_account(self, email: str, password: str) -> Account:
|
|
"""Create a new user account, hashing the plain-text password before storage.
|
|
|
|
Raises ``ValueError`` if the email address is already registered, so the
|
|
caller does not need to catch SQLAlchemy exceptions directly.
|
|
|
|
Usage::
|
|
|
|
try:
|
|
account = await service.create_account("alice@example.com", "s3cr3t")
|
|
except ValueError:
|
|
# email already taken
|
|
...
|
|
"""
|
|
try:
|
|
user = await user_repository.create(
|
|
self.db,
|
|
email=email,
|
|
hashed_password=hash_password(password),
|
|
)
|
|
except IntegrityError:
|
|
await self.db.rollback()
|
|
raise ValueError("Email already registered")
|
|
|
|
return Account(
|
|
id=str(user.id),
|
|
email=user.email,
|
|
is_active=user.is_active,
|
|
is_email_verified=user.is_email_verified,
|
|
created_at=user.created_at,
|
|
)
|
|
|
|
async def get_account(self, user_id: uuid.UUID) -> Account:
|
|
"""Retrieve a user's account profile including all their learnable languages.
|
|
|
|
Raises ``ValueError`` if no user exists for the given ``user_id``.
|
|
|
|
Usage::
|
|
|
|
account = await service.get_account(user_id)
|
|
for lang in account.learnable_languages:
|
|
print(lang.target_language, lang.proficiencies)
|
|
"""
|
|
# user_repository only exposes get_by_email; query by id directly
|
|
result = await self.db.execute(
|
|
select(UserEntity).where(UserEntity.id == user_id)
|
|
)
|
|
user = result.scalar_one_or_none()
|
|
if user is None:
|
|
raise ValueError(f"User {user_id} not found")
|
|
|
|
languages = await learnable_language_repository.list_for_user(self.db, user_id)
|
|
|
|
return Account(
|
|
id=str(user.id),
|
|
email=user.email,
|
|
is_active=user.is_active,
|
|
is_email_verified=user.is_email_verified,
|
|
created_at=user.created_at,
|
|
learnable_languages=languages,
|
|
)
|
|
|
|
async def add_learnable_language(
|
|
self,
|
|
user_id: uuid.UUID,
|
|
source_language: str,
|
|
target_language: str,
|
|
proficiencies: list[str],
|
|
) -> LearnableLanguage:
|
|
"""Add a language pair to the user's account, or update proficiency levels if
|
|
the pair already exists (upsert semantics).
|
|
|
|
Usage::
|
|
|
|
lang = await service.add_learnable_language(
|
|
user_id,
|
|
source_language="en",
|
|
target_language="fr",
|
|
proficiencies=["B1", "B2"],
|
|
)
|
|
print(lang.id) # UUID of the learnable_language row
|
|
"""
|
|
return await learnable_language_repository.upsert(
|
|
self.db,
|
|
user_id=user_id,
|
|
source_language=source_language,
|
|
target_language=target_language,
|
|
proficiencies=proficiencies,
|
|
)
|
|
|
|
async def remove_learnable_language(
|
|
self, user_id: uuid.UUID, language_id: uuid.UUID
|
|
) -> None:
|
|
"""Remove a learnable language from the user's account by its row ID.
|
|
|
|
Raises ``ValueError`` if the language entry does not exist or does not belong
|
|
to ``user_id``.
|
|
|
|
Usage::
|
|
|
|
await service.remove_learnable_language(user_id, lang.id)
|
|
"""
|
|
deleted = await learnable_language_repository.delete(
|
|
self.db, user_id=user_id, language_id=language_id
|
|
)
|
|
if not deleted:
|
|
raise ValueError(f"Learnable language {language_id} not found for this user")
|