Coverage for polar/user/schemas.py: 94%
49 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 12:42 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 12:42 +0000
1import hashlib
2import hmac
3import uuid
4from enum import StrEnum
5from typing import Annotated, Literal
7from fastapi import Depends
8from pydantic import UUID4, EmailStr, Field, computed_field
10from polar.auth.scope import Scope
11from polar.config import settings
12from polar.kit.schemas import Schema, TimestampedSchema, UUID4ToStr
13from polar.models.user import IdentityVerificationStatus, OAuthPlatform
16class UserBase(Schema):
17 email: EmailStr
18 avatar_url: str | None
19 account_id: UUID4 | None
22class OAuthAccountRead(TimestampedSchema):
23 platform: OAuthPlatform
24 account_id: str
25 account_email: str
26 account_username: str | None
29class UserRead(UserBase, TimestampedSchema):
30 id: uuid.UUID
31 accepted_terms_of_service: bool
32 is_admin: bool
33 identity_verified: bool
34 identity_verification_status: IdentityVerificationStatus
35 oauth_accounts: list[OAuthAccountRead]
37 @computed_field
38 def email_hash(self) -> str | None:
39 if settings.PLAIN_CHAT_SECRET is None: 39 ↛ 41line 39 didn't jump to line 41 because the condition on line 39 was always true
40 return None
41 message = hmac.new(
42 settings.PLAIN_CHAT_SECRET.encode("utf-8"),
43 self.email.encode("utf-8"),
44 hashlib.sha256,
45 )
46 return message.hexdigest()
49class UserIdentityVerification(Schema):
50 id: str
51 client_secret: str
54class UserSetAccount(Schema):
55 account_id: UUID4
58class UserStripePortalSession(Schema):
59 url: str
62class UserScopes(Schema):
63 scopes: list[Scope]
66###############################################################################
67# USER ATTRIBUTION
68###############################################################################
71class UserSignupAttribution(Schema):
72 intent: (
73 Literal[
74 "creator",
75 "pledge",
76 "purchase",
77 "subscription",
78 "newsletter_subscription",
79 ]
80 | None
81 ) = None
83 # Flywheel sources
84 order: UUID4ToStr | None = None
85 subscription: UUID4ToStr | None = None
86 pledge: UUID4ToStr | None = None
87 from_storefront: UUID4ToStr | None = None
89 # Website source
90 path: str | None = None
91 host: str | None = None
93 # UTM parameters
94 utm_source: str | None = None
95 utm_medium: str | None = None
96 utm_campaign: str | None = None
98 campaign: str | None = None
101UserSignupAttributionQueryJSON = str | None
104async def get_signup_attribution(
105 attribution: UserSignupAttributionQueryJSON = None,
106) -> UserSignupAttribution | None:
107 if attribution:
108 return UserSignupAttribution.model_validate_json(attribution)
109 return None
112UserSignupAttributionQuery = Annotated[
113 UserSignupAttribution | None, Depends(get_signup_attribution)
114]
117class UserDeletionBlockedReason(StrEnum):
118 """Reasons why a user account cannot be immediately deleted."""
120 HAS_ACTIVE_ORGANIZATIONS = "has_active_organizations"
123class BlockingOrganization(Schema):
124 """Organization that is blocking user deletion."""
126 id: UUID4
127 slug: str
128 name: str
131class UserDeletionResponse(Schema):
132 """Response for user deletion request."""
134 deleted: bool = Field(
135 description="Whether the user account was immediately deleted"
136 )
137 blocked_reasons: list[UserDeletionBlockedReason] = Field(
138 default_factory=list,
139 description="Reasons why immediate deletion is blocked",
140 )
141 blocking_organizations: list[BlockingOrganization] = Field(
142 default_factory=list,
143 description="Organizations that must be deleted first",
144 )