Compare commits

...

3 Commits

Author SHA1 Message Date
36309c2460 Merge feature/birthday-sync: sync DOB to umbral contacts on profile/settings change 2026-03-07 06:19:16 +08:00
66cc1a0457 Action QA findings: refactor sync to accept resolved values
C-01: sync_birthday_to_contacts now accepts (share_birthday, date_of_birth)
      directly — no internal re-query, no stale-read risk with autoflush.
W-01: Eliminated redundant User/Settings SELECTs inside the service.
W-02: Removed scalar_one() on User query (no longer queries internally).
W-03: Settings router only syncs when share_birthday value actually changes.
S-02: Added logger.info with rowcount for observability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 06:13:21 +08:00
8aec5a5078 Sync birthday to umbral contacts on DOB or share_birthday change
When a user updates their date of birth or toggles share_birthday,
all linked Person records (where linked_user_id matches) are updated.
If share_birthday is off, the birthday is cleared on linked records.
Virtual birthday events auto-reflect the change on next calendar poll.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 06:01:35 +08:00
3 changed files with 38 additions and 0 deletions

View File

@ -25,6 +25,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func from sqlalchemy import select, func
from app.database import get_db from app.database import get_db
from app.services.connection import sync_birthday_to_contacts
from app.models.user import User from app.models.user import User
from app.models.session import UserSession from app.models.session import UserSession
from app.models.settings import Settings from app.models.settings import Settings
@ -686,6 +687,12 @@ async def update_profile(
current_user.email = update_data["email"] current_user.email = update_data["email"]
if "date_of_birth" in update_data: if "date_of_birth" in update_data:
current_user.date_of_birth = update_data["date_of_birth"] current_user.date_of_birth = update_data["date_of_birth"]
settings_result = await db.execute(
select(Settings).where(Settings.user_id == current_user.id)
)
user_settings = settings_result.scalar_one_or_none()
share = user_settings.share_birthday if user_settings else False
await sync_birthday_to_contacts(db, current_user.id, share_birthday=share, date_of_birth=update_data["date_of_birth"])
if "umbral_name" in update_data: if "umbral_name" in update_data:
current_user.umbral_name = update_data["umbral_name"] current_user.umbral_name = update_data["umbral_name"]

View File

@ -7,6 +7,7 @@ from app.models.settings import Settings
from app.models.user import User from app.models.user import User
from app.schemas.settings import SettingsUpdate, SettingsResponse from app.schemas.settings import SettingsUpdate, SettingsResponse
from app.routers.auth import get_current_user, get_current_settings from app.routers.auth import get_current_user, get_current_settings
from app.services.connection import sync_birthday_to_contacts
router = APIRouter() router = APIRouter()
@ -78,6 +79,7 @@ async def get_settings(
async def update_settings( async def update_settings(
settings_update: SettingsUpdate, settings_update: SettingsUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
current_settings: Settings = Depends(get_current_settings) current_settings: Settings = Depends(get_current_settings)
): ):
"""Update settings.""" """Update settings."""
@ -91,9 +93,18 @@ async def update_settings(
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
old_share_birthday = current_settings.share_birthday
for key, value in update_data.items(): for key, value in update_data.items():
setattr(current_settings, key, value) setattr(current_settings, key, value)
if "share_birthday" in update_data and update_data["share_birthday"] != old_share_birthday:
await sync_birthday_to_contacts(
db, current_user.id,
share_birthday=update_data["share_birthday"],
date_of_birth=current_user.date_of_birth,
)
await db.commit() await db.commit()
await db.refresh(current_settings) await db.refresh(current_settings)

View File

@ -9,6 +9,7 @@ from datetime import date as date_type
from types import SimpleNamespace from types import SimpleNamespace
from typing import Optional from typing import Optional
from sqlalchemy import update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.models.person import Person from app.models.person import Person
@ -134,6 +135,25 @@ def create_person_from_connection(
) )
async def sync_birthday_to_contacts(
db: AsyncSession,
user_id: int,
share_birthday: bool,
date_of_birth: Optional[date_type],
) -> None:
"""Sync user's DOB to all Person records where linked_user_id == user_id.
Caller passes resolved values no internal re-query."""
new_birthday = date_of_birth if share_birthday else None
result = await db.execute(
update(Person)
.where(Person.linked_user_id == user_id)
.values(birthday=new_birthday)
)
logger.info("sync_birthday_to_contacts user_id=%s updated %s person(s)", user_id, result.rowcount)
async def detach_umbral_contact(person: Person) -> None: async def detach_umbral_contact(person: Person) -> None:
"""Convert an umbral contact back to a standard contact. Does NOT commit. """Convert an umbral contact back to a standard contact. Does NOT commit.