feat(auth): add local user and household models
This commit is contained in:
parent
e29d62f949
commit
04daadccda
13 changed files with 178 additions and 17 deletions
1
.sisyphus/evidence/task-T1-identity-models.txt
Normal file
1
.sisyphus/evidence/task-T1-identity-models.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
['household_memberships', 'households', 'users']
|
||||
1
.sisyphus/evidence/task-T1-sharing-default.txt
Normal file
1
.sisyphus/evidence/task-T1-sharing-default.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
False
|
||||
|
|
@ -6,6 +6,7 @@ from .enums import (
|
|||
DayTime,
|
||||
EvidenceLevel,
|
||||
GroomingAction,
|
||||
HouseholdRole,
|
||||
IngredientFunction,
|
||||
MedicationKind,
|
||||
OverallSkinState,
|
||||
|
|
@ -14,6 +15,7 @@ from .enums import (
|
|||
ProductCategory,
|
||||
RemainingLevel,
|
||||
ResultFlag,
|
||||
Role,
|
||||
RoutineRole,
|
||||
SexAtBirth,
|
||||
SkinConcern,
|
||||
|
|
@ -24,6 +26,8 @@ from .enums import (
|
|||
UsageFrequency,
|
||||
)
|
||||
from .health import LabResult, MedicationEntry, MedicationUsage
|
||||
from .household import Household
|
||||
from .household_membership import HouseholdMembership
|
||||
from .pricing import PricingRecalcJob
|
||||
from .product import (
|
||||
ActiveIngredient,
|
||||
|
|
@ -42,6 +46,7 @@ from .skincare import (
|
|||
SkinConditionSnapshotBase,
|
||||
SkinConditionSnapshotPublic,
|
||||
)
|
||||
from .user import User
|
||||
|
||||
__all__ = [
|
||||
# ai logs
|
||||
|
|
@ -54,6 +59,7 @@ __all__ = [
|
|||
"DayTime",
|
||||
"EvidenceLevel",
|
||||
"GroomingAction",
|
||||
"HouseholdRole",
|
||||
"IngredientFunction",
|
||||
"MedicationKind",
|
||||
"OverallSkinState",
|
||||
|
|
@ -62,6 +68,7 @@ __all__ = [
|
|||
"RemainingLevel",
|
||||
"ProductCategory",
|
||||
"ResultFlag",
|
||||
"Role",
|
||||
"RoutineRole",
|
||||
"SexAtBirth",
|
||||
"SkinConcern",
|
||||
|
|
@ -74,6 +81,8 @@ __all__ = [
|
|||
"LabResult",
|
||||
"MedicationEntry",
|
||||
"MedicationUsage",
|
||||
"Household",
|
||||
"HouseholdMembership",
|
||||
# product
|
||||
"ActiveIngredient",
|
||||
"Product",
|
||||
|
|
@ -85,6 +94,7 @@ __all__ = [
|
|||
"ProductWithInventory",
|
||||
"PricingRecalcJob",
|
||||
"UserProfile",
|
||||
"User",
|
||||
# routine
|
||||
"GroomingSchedule",
|
||||
"Routine",
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ from .domain import Domain
|
|||
|
||||
|
||||
class AICallLog(SQLModel, table=True):
|
||||
__tablename__ = "ai_call_logs"
|
||||
__tablename__ = "ai_call_logs" # pyright: ignore[reportAssignmentType]
|
||||
__domains__: ClassVar[frozenset[Domain]] = frozenset()
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
user_id: UUID | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
created_at: datetime = Field(default_factory=utc_now, nullable=False)
|
||||
endpoint: str = Field(index=True)
|
||||
model: str
|
||||
|
|
|
|||
|
|
@ -29,6 +29,16 @@ class UsageFrequency(str, Enum):
|
|||
AS_NEEDED = "as_needed"
|
||||
|
||||
|
||||
class Role(str, Enum):
|
||||
ADMIN = "admin"
|
||||
MEMBER = "member"
|
||||
|
||||
|
||||
class HouseholdRole(str, Enum):
|
||||
OWNER = "owner"
|
||||
MEMBER = "member"
|
||||
|
||||
|
||||
class ProductCategory(str, Enum):
|
||||
CLEANSER = "cleanser"
|
||||
TONER = "toner"
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@ from .enums import MedicationKind, ResultFlag
|
|||
|
||||
|
||||
class MedicationEntry(SQLModel, table=True):
|
||||
__tablename__ = "medication_entries"
|
||||
__tablename__ = "medication_entries" # pyright: ignore[reportAssignmentType]
|
||||
__domains__: ClassVar[frozenset[Domain]] = frozenset({Domain.HEALTH})
|
||||
|
||||
record_id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
user_id: UUID | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
|
||||
kind: MedicationKind = Field(index=True)
|
||||
|
||||
|
|
@ -43,10 +44,11 @@ class MedicationEntry(SQLModel, table=True):
|
|||
|
||||
|
||||
class MedicationUsage(SQLModel, table=True):
|
||||
__tablename__ = "medication_usages"
|
||||
__tablename__ = "medication_usages" # pyright: ignore[reportAssignmentType]
|
||||
__domains__: ClassVar[frozenset[Domain]] = frozenset({Domain.HEALTH})
|
||||
|
||||
record_id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
user_id: UUID | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
medication_record_id: UUID = Field(
|
||||
foreign_key="medication_entries.record_id", index=True
|
||||
)
|
||||
|
|
@ -78,10 +80,11 @@ class MedicationUsage(SQLModel, table=True):
|
|||
|
||||
|
||||
class LabResult(SQLModel, table=True):
|
||||
__tablename__ = "lab_results"
|
||||
__tablename__ = "lab_results" # pyright: ignore[reportAssignmentType]
|
||||
__domains__: ClassVar[frozenset[Domain]] = frozenset({Domain.HEALTH})
|
||||
|
||||
record_id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
user_id: UUID | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
|
||||
collected_at: datetime = Field(index=True)
|
||||
test_code: str = Field(index=True, regex=r"^\d+-\d$")
|
||||
|
|
|
|||
36
backend/innercontext/models/household.py
Normal file
36
backend/innercontext/models/household.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# pyright: reportImportCycles=false
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, ClassVar
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import Column, DateTime
|
||||
from sqlmodel import Field, Relationship, SQLModel
|
||||
|
||||
from .base import utc_now
|
||||
from .domain import Domain
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .household_membership import HouseholdMembership # pyright: ignore[reportImportCycles]
|
||||
|
||||
|
||||
class Household(SQLModel, table=True):
|
||||
__tablename__ = "households" # pyright: ignore[reportAssignmentType]
|
||||
__domains__: ClassVar[frozenset[Domain]] = frozenset()
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
created_at: datetime = Field(default_factory=utc_now, nullable=False)
|
||||
updated_at: datetime = Field(
|
||||
default_factory=utc_now,
|
||||
sa_column=Column(
|
||||
DateTime(timezone=True),
|
||||
default=utc_now,
|
||||
onupdate=utc_now,
|
||||
nullable=False,
|
||||
),
|
||||
)
|
||||
|
||||
memberships: list["HouseholdMembership"] = Relationship(
|
||||
back_populates="household",
|
||||
sa_relationship_kwargs={"cascade": "all, delete-orphan"},
|
||||
)
|
||||
45
backend/innercontext/models/household_membership.py
Normal file
45
backend/innercontext/models/household_membership.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# pyright: reportImportCycles=false
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, ClassVar
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import Column, DateTime, UniqueConstraint
|
||||
from sqlmodel import Field, Relationship, SQLModel
|
||||
|
||||
from .base import utc_now
|
||||
from .domain import Domain
|
||||
from .enums import HouseholdRole
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .household import Household # pyright: ignore[reportImportCycles]
|
||||
from .user import User # pyright: ignore[reportImportCycles]
|
||||
|
||||
|
||||
class HouseholdMembership(SQLModel, table=True):
|
||||
__tablename__ = "household_memberships" # pyright: ignore[reportAssignmentType]
|
||||
__domains__: ClassVar[frozenset[Domain]] = frozenset()
|
||||
__table_args__ = (
|
||||
UniqueConstraint("user_id", name="uq_household_memberships_user_id"),
|
||||
)
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
user_id: UUID = Field(foreign_key="users.id", index=True, ondelete="CASCADE")
|
||||
household_id: UUID = Field(
|
||||
foreign_key="households.id", index=True, ondelete="CASCADE"
|
||||
)
|
||||
role: HouseholdRole = Field(default=HouseholdRole.MEMBER, index=True)
|
||||
|
||||
created_at: datetime = Field(default_factory=utc_now, nullable=False)
|
||||
updated_at: datetime = Field(
|
||||
default_factory=utc_now,
|
||||
sa_column=Column(
|
||||
DateTime(timezone=True),
|
||||
default=utc_now,
|
||||
onupdate=utc_now,
|
||||
nullable=False,
|
||||
),
|
||||
)
|
||||
|
||||
user: "User" = Relationship(back_populates="household_membership")
|
||||
household: "Household" = Relationship(back_populates="memberships")
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
from datetime import date, datetime
|
||||
from enum import Enum
|
||||
from typing import Any, ClassVar, Optional, cast
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
|
|
@ -72,7 +73,9 @@ class ProductContext(SQLModel):
|
|||
|
||||
def _ev(v: object) -> str:
|
||||
"""Return enum value or string as-is (handles both DB-loaded dicts and Python enums)."""
|
||||
return v.value if hasattr(v, "value") else str(v) # type: ignore[union-attr]
|
||||
if isinstance(v, Enum):
|
||||
return str(v.value)
|
||||
return str(v)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -136,10 +139,11 @@ class ProductBase(SQLModel):
|
|||
|
||||
|
||||
class Product(ProductBase, table=True):
|
||||
__tablename__ = "products"
|
||||
__tablename__ = "products" # pyright: ignore[reportAssignmentType]
|
||||
__domains__: ClassVar[frozenset[Domain]] = frozenset({Domain.SKINCARE})
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
user_id: UUID | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
short_id: str = Field(
|
||||
max_length=8,
|
||||
unique=True,
|
||||
|
|
@ -230,8 +234,8 @@ class Product(ProductBase, table=True):
|
|||
*,
|
||||
computed_price_tier: PriceTier | None = None,
|
||||
price_per_use_pln: float | None = None,
|
||||
) -> dict:
|
||||
ctx: dict = {
|
||||
) -> dict[str, Any]:
|
||||
ctx: dict[str, Any] = {
|
||||
"id": str(self.id),
|
||||
"name": self.name,
|
||||
"brand": self.brand,
|
||||
|
|
@ -273,7 +277,7 @@ class Product(ProductBase, table=True):
|
|||
if isinstance(a, dict):
|
||||
actives_ctx.append(a)
|
||||
else:
|
||||
a_dict: dict = {"name": a.name}
|
||||
a_dict: dict[str, Any] = {"name": a.name}
|
||||
if a.percent is not None:
|
||||
a_dict["percent"] = a.percent
|
||||
if a.functions:
|
||||
|
|
@ -342,8 +346,10 @@ class Product(ProductBase, table=True):
|
|||
inv for inv in (self.inventory or []) if inv.is_opened and inv.opened_at
|
||||
]
|
||||
if opened_items:
|
||||
most_recent = max(opened_items, key=lambda x: x.opened_at)
|
||||
ctx["days_since_opened"] = (date.today() - most_recent.opened_at).days
|
||||
most_recent = max(opened_items, key=lambda x: cast(date, x.opened_at))
|
||||
ctx["days_since_opened"] = (
|
||||
date.today() - cast(date, most_recent.opened_at)
|
||||
).days
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
@ -351,12 +357,14 @@ class Product(ProductBase, table=True):
|
|||
|
||||
|
||||
class ProductInventory(SQLModel, table=True):
|
||||
__tablename__ = "product_inventory"
|
||||
__tablename__ = "product_inventory" # pyright: ignore[reportAssignmentType]
|
||||
__domains__: ClassVar[frozenset[Domain]] = frozenset({Domain.SKINCARE})
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
user_id: UUID | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
product_id: UUID = Field(foreign_key="products.id", index=True, ondelete="CASCADE")
|
||||
|
||||
is_household_shared: bool = Field(default=False)
|
||||
is_opened: bool = Field(default=False)
|
||||
opened_at: date | None = Field(default=None)
|
||||
finished_at: date | None = Field(default=None)
|
||||
|
|
|
|||
|
|
@ -11,12 +11,13 @@ from .enums import SexAtBirth
|
|||
|
||||
|
||||
class UserProfile(SQLModel, table=True):
|
||||
__tablename__ = "user_profiles"
|
||||
__tablename__ = "user_profiles" # pyright: ignore[reportAssignmentType]
|
||||
__domains__: ClassVar[frozenset[Domain]] = frozenset(
|
||||
{Domain.HEALTH, Domain.SKINCARE}
|
||||
)
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
user_id: UUID | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
birth_date: date | None = Field(default=None)
|
||||
sex_at_birth: SexAtBirth | None = Field(
|
||||
default=None,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class Routine(SQLModel, table=True):
|
||||
__tablename__ = "routines"
|
||||
__tablename__ = "routines" # pyright: ignore[reportAssignmentType]
|
||||
__domains__: ClassVar[frozenset[Domain]] = frozenset({Domain.SKINCARE})
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
|
|
@ -23,6 +23,7 @@ class Routine(SQLModel, table=True):
|
|||
)
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
user_id: UUID | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
routine_date: date = Field(index=True)
|
||||
part_of_day: PartOfDay = Field(index=True)
|
||||
notes: str | None = Field(default=None)
|
||||
|
|
@ -45,20 +46,22 @@ class Routine(SQLModel, table=True):
|
|||
|
||||
|
||||
class GroomingSchedule(SQLModel, table=True):
|
||||
__tablename__ = "grooming_schedule"
|
||||
__tablename__ = "grooming_schedule" # pyright: ignore[reportAssignmentType]
|
||||
__domains__: ClassVar[frozenset[Domain]] = frozenset({Domain.SKINCARE})
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
user_id: UUID | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
day_of_week: int = Field(ge=0, le=6, index=True) # 0 = poniedziałek, 6 = niedziela
|
||||
action: GroomingAction
|
||||
notes: str | None = Field(default=None)
|
||||
|
||||
|
||||
class RoutineStep(SQLModel, table=True):
|
||||
__tablename__ = "routine_steps"
|
||||
__tablename__ = "routine_steps" # pyright: ignore[reportAssignmentType]
|
||||
__domains__: ClassVar[frozenset[Domain]] = frozenset({Domain.SKINCARE})
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
user_id: UUID | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
routine_id: UUID = Field(foreign_key="routines.id", index=True)
|
||||
product_id: UUID | None = Field(default=None, foreign_key="products.id", index=True)
|
||||
order_index: int = Field(ge=0)
|
||||
|
|
|
|||
|
|
@ -51,11 +51,12 @@ class SkinConditionSnapshot(SkinConditionSnapshotBase, table=True):
|
|||
i kontekstu rutyny. Wszystkie metryki numeryczne w skali 1–5.
|
||||
"""
|
||||
|
||||
__tablename__ = "skin_condition_snapshots"
|
||||
__tablename__ = "skin_condition_snapshots" # pyright: ignore[reportAssignmentType]
|
||||
__domains__: ClassVar[frozenset[Domain]] = frozenset({Domain.SKINCARE})
|
||||
__table_args__ = (UniqueConstraint("snapshot_date", name="uq_skin_snapshot_date"),)
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
user_id: UUID | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
|
||||
# Override: add index for table context
|
||||
snapshot_date: date = Field(index=True)
|
||||
|
|
|
|||
41
backend/innercontext/models/user.py
Normal file
41
backend/innercontext/models/user.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# pyright: reportImportCycles=false
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, ClassVar
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import Column, DateTime, String, UniqueConstraint
|
||||
from sqlmodel import Field, Relationship, SQLModel
|
||||
|
||||
from .base import utc_now
|
||||
from .domain import Domain
|
||||
from .enums import Role
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .household_membership import HouseholdMembership # pyright: ignore[reportImportCycles]
|
||||
|
||||
|
||||
class User(SQLModel, table=True):
|
||||
__tablename__ = "users" # pyright: ignore[reportAssignmentType]
|
||||
__domains__: ClassVar[frozenset[Domain]] = frozenset()
|
||||
__table_args__ = (
|
||||
UniqueConstraint("oidc_issuer", "oidc_subject", name="uq_users_oidc_identity"),
|
||||
)
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
oidc_issuer: str = Field(sa_column=Column(String(length=512), nullable=False))
|
||||
oidc_subject: str = Field(sa_column=Column(String(length=512), nullable=False))
|
||||
role: Role = Field(default=Role.MEMBER, index=True)
|
||||
|
||||
created_at: datetime = Field(default_factory=utc_now, nullable=False)
|
||||
updated_at: datetime = Field(
|
||||
default_factory=utc_now,
|
||||
sa_column=Column(
|
||||
DateTime(timezone=True),
|
||||
default=utc_now,
|
||||
onupdate=utc_now,
|
||||
nullable=False,
|
||||
),
|
||||
)
|
||||
|
||||
household_membership: "HouseholdMembership" = Relationship(back_populates="user")
|
||||
Loading…
Add table
Add a link
Reference in a new issue