email wiring

This commit is contained in:
debu-2 2026-10-04 20:50:47 +05:30
parent ee23b71f0f
commit 42cd937f0a
12 changed files with 494 additions and 45 deletions

View file

@ -8,11 +8,13 @@ from datetime import date
from pwdlib import PasswordHash
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models import Account
from app.schemas import OrgMemberCreate, OrgMemberRead
from app.services.domain_provisioning import account_is_domain_owner
from app.services.org_invitations import send_employee_invitation
from app.services.org_sections import get_org_section
password_hasher = PasswordHash.recommended()
PLACEHOLDER_BIRTHDATE = date(1990, 1, 1)
@ -38,6 +40,8 @@ async def can_manage_org_members(
async def member_read(
session: AsyncSession,
member: Account,
*,
invitation_sent: bool = False,
) -> OrgMemberRead:
is_owner = await account_is_domain_owner(
session,
@ -55,7 +59,9 @@ async def member_read(
member_status=member.member_status,
must_change_password=member.must_change_password,
is_domain_owner=is_owner,
groups=[],
section_id=member.section_id,
section_name=member.section.name if member.section else None,
invitation_sent=invitation_sent,
created_at=member.created_at,
)
@ -66,6 +72,7 @@ async def list_org_members(
) -> tuple[list[OrgMemberRead], dict[str, int]]:
result = await session.scalars(
select(Account)
.options(selectinload(Account.section))
.where(
Account.account_type == "company",
Account.company_domain == company_domain,
@ -124,6 +131,12 @@ async def create_org_member(
if existing:
raise ValueError("That email address is already registered.")
section = None
if payload.section_id:
section = await get_org_section(session, company_domain, payload.section_id)
if section is None:
raise ValueError("Selected section was not found.")
phone_number = _placeholder_phone()
while await session.scalar(
select(Account).where(Account.phone_number == phone_number)
@ -142,22 +155,32 @@ async def create_org_member(
member_status=member_status,
invitation_email=invitation_email,
must_change_password=payload.force_password_change,
section_id=section.id if section else None,
created_by_id=manager.id,
password_hash=password_hasher.hash(payload.password),
)
session.add(member)
await session.flush()
if section:
member.section = section
invitation_sent = False
if payload.send_invitation:
await send_employee_invitation(
session,
member=member,
manager=manager,
initial_password=payload.password,
force_password_change=payload.force_password_change,
)
try:
await send_employee_invitation(
session,
member=member,
manager=manager,
initial_password=payload.password,
force_password_change=payload.force_password_change,
)
invitation_sent = True
except Exception as error:
raise ValueError(
f"Could not send invitation email: {error}"
) from error
return await member_read(session, member)
return await member_read(session, member, invitation_sent=invitation_sent)
async def update_org_member_role(

View file

@ -0,0 +1,77 @@
"""Department sections within an organization."""
from __future__ import annotations
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import OrgSection
from app.schemas import OrgSectionCreate, OrgSectionRead
DEFAULT_SECTIONS = ["Admin", "HR", "Engineering", "Finance"]
async def ensure_default_sections(
session: AsyncSession,
company_domain: str,
) -> None:
existing = await session.scalar(
select(OrgSection.id)
.where(OrgSection.company_domain == company_domain)
.limit(1)
)
if existing:
return
for name in DEFAULT_SECTIONS:
session.add(OrgSection(company_domain=company_domain, name=name))
await session.flush()
async def list_org_sections(
session: AsyncSession,
company_domain: str,
) -> list[OrgSectionRead]:
await ensure_default_sections(session, company_domain)
result = await session.scalars(
select(OrgSection)
.where(OrgSection.company_domain == company_domain)
.order_by(OrgSection.name.asc())
)
return [OrgSectionRead.model_validate(section) for section in result.all()]
async def create_org_section(
session: AsyncSession,
company_domain: str,
payload: OrgSectionCreate,
) -> OrgSectionRead:
name = payload.name.strip()
if not name:
raise ValueError("Section name is required.")
existing = await session.scalar(
select(OrgSection).where(
OrgSection.company_domain == company_domain,
OrgSection.name.ilike(name),
)
)
if existing:
raise ValueError("That section already exists.")
section = OrgSection(company_domain=company_domain, name=name)
session.add(section)
await session.flush()
return OrgSectionRead.model_validate(section)
async def get_org_section(
session: AsyncSession,
company_domain: str,
section_id,
) -> OrgSection | None:
return await session.scalar(
select(OrgSection).where(
OrgSection.id == section_id,
OrgSection.company_domain == company_domain,
)
)