language-learning-app/api/app/domain/services/account_service.py

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")