Coverage for polar/models/account.py: 68%
88 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
1from enum import StrEnum
2from typing import TYPE_CHECKING, Any, TypeAlias
3from uuid import UUID
5from sqlalchemy import Boolean, ForeignKey, Integer, String, Text, Uuid
6from sqlalchemy.dialects.postgresql import JSONB
7from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship
9from polar.config import settings
10from polar.enums import AccountType
11from polar.kit.address import Address, AddressType
12from polar.kit.db.models import RecordModel
13from polar.kit.extensions.sqlalchemy import StringEnum
14from polar.kit.math import polar_round
16if TYPE_CHECKING: 16 ↛ 17line 16 didn't jump to line 17 because the condition on line 16 was never true
17 from .organization import Organization
18 from .user import User
21FeeBasisPoints: TypeAlias = int
22FeeFixedCents: TypeAlias = int
23Fees: TypeAlias = tuple[FeeBasisPoints, FeeFixedCents]
26class Account(RecordModel):
27 class Status(StrEnum):
28 CREATED = "created"
29 ONBOARDING_STARTED = "onboarding_started"
30 UNDER_REVIEW = "under_review"
31 DENIED = "denied"
32 ACTIVE = "active"
34 def get_display_name(self) -> str:
35 return {
36 Account.Status.CREATED: "Created",
37 Account.Status.ONBOARDING_STARTED: "Onboarding Started",
38 Account.Status.UNDER_REVIEW: "Under Review",
39 Account.Status.DENIED: "Denied",
40 Account.Status.ACTIVE: "Active",
41 }[self]
43 __tablename__ = "accounts"
45 account_type: Mapped[AccountType] = mapped_column(
46 StringEnum(AccountType), nullable=False
47 )
49 admin_id: Mapped[UUID] = mapped_column(Uuid, ForeignKey("users.id", use_alter=True))
51 stripe_id: Mapped[str | None] = mapped_column(
52 String(100), nullable=True, default=None
53 )
54 open_collective_slug: Mapped[str | None] = mapped_column(
55 String(255), nullable=True, default=None
56 )
58 email: Mapped[str | None] = mapped_column(String(254), nullable=True, default=None)
60 country: Mapped[str] = mapped_column(String(2), nullable=False)
61 currency: Mapped[str] = mapped_column(String(3))
63 is_details_submitted: Mapped[bool] = mapped_column(Boolean, nullable=False)
64 is_charges_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False)
65 is_payouts_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False)
67 processor_fees_applicable: Mapped[bool] = mapped_column(
68 Boolean, nullable=False, default=True
69 )
70 _platform_fee_percent: Mapped[int | None] = mapped_column(
71 Integer, name="platform_fee_percent", nullable=True, default=None
72 )
73 _platform_fee_fixed: Mapped[int | None] = mapped_column(
74 Integer, name="platform_fee_fixed", nullable=True, default=None
75 )
77 business_type: Mapped[str | None] = mapped_column(
78 String(255), nullable=True, default=None
79 )
81 status: Mapped[Status] = mapped_column(
82 StringEnum(Status), nullable=False, default=Status.CREATED
83 )
84 next_review_threshold: Mapped[int | None] = mapped_column(
85 Integer, nullable=True, default=0
86 )
87 campaign_id: Mapped[UUID | None] = mapped_column(
88 Uuid,
89 ForeignKey("campaigns.id", ondelete="set null"),
90 default=None,
91 index=True,
92 )
94 data: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
96 billing_name: Mapped[str | None] = mapped_column(
97 String, nullable=True, default=None
98 )
99 billing_address: Mapped[Address | None] = mapped_column(AddressType, nullable=True)
100 billing_additional_info: Mapped[str | None] = mapped_column(
101 Text, nullable=True, default=None
102 )
103 billing_notes: Mapped[str | None] = mapped_column(Text, nullable=True, default=None)
105 @declared_attr
106 def admin(cls) -> Mapped["User"]:
107 return relationship("User", lazy="raise", foreign_keys="[Account.admin_id]")
109 @declared_attr
110 def users(cls) -> Mapped[list["User"]]:
111 return relationship(
112 "User",
113 lazy="raise",
114 back_populates="account",
115 foreign_keys="[User.account_id]",
116 )
118 @declared_attr
119 def all_organizations(cls) -> Mapped[list["Organization"]]:
120 return relationship("Organization", lazy="raise", back_populates="account")
122 @declared_attr
123 def organizations(cls) -> Mapped[list["Organization"]]:
124 return relationship(
125 "Organization",
126 lazy="raise",
127 primaryjoin=(
128 "and_("
129 "Organization.account_id == Account.id,"
130 "Organization.deleted_at.is_(None)"
131 ")"
132 ),
133 viewonly=True,
134 )
136 def is_active(self) -> bool:
137 return self.status == Account.Status.ACTIVE
139 def is_under_review(self) -> bool:
140 return self.status == Account.Status.UNDER_REVIEW
142 def is_payout_ready(self) -> bool:
143 return self.is_active() and (
144 # For Stripe accounts, check if payouts are enabled.
145 # Normally, the account shouldn't be active if payouts are not enabled
146 # but let's be extra cautious
147 self.account_type != AccountType.stripe or self.is_payouts_enabled
148 )
150 def get_associations_names(self) -> list[str]:
151 associations_names: list[str] = []
152 for user in self.users:
153 associations_names.append(user.email)
154 for organization in self.organizations:
155 associations_names.append(organization.slug)
156 return associations_names
158 @property
159 def platform_fee(self) -> Fees:
160 basis_points = self._platform_fee_percent
161 if basis_points is None:
162 basis_points = settings.PLATFORM_FEE_BASIS_POINTS
164 fixed = self._platform_fee_fixed
165 if fixed is None:
166 fixed = settings.PLATFORM_FEE_FIXED
168 return (basis_points, fixed)
170 def calculate_fee_in_cents(self, amount_in_cents: int) -> int:
171 basis_points, fixed = self.platform_fee
172 fee_in_cents = (amount_in_cents * (basis_points / 10_000)) + fixed
173 # Apply same logic as Stripe fee rounding
174 return polar_round(fee_in_cents)