Coverage for polar/models/account.py: 68%
88 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-12-05 16:17 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-12-05 16:17 +0000
1from enum import StrEnum 1ab
2from typing import TYPE_CHECKING, Any, TypeAlias 1ab
3from uuid import UUID 1ab
5from sqlalchemy import Boolean, ForeignKey, Integer, String, Text, Uuid 1ab
6from sqlalchemy.dialects.postgresql import JSONB 1ab
7from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship 1ab
9from polar.config import settings 1ab
10from polar.enums import AccountType 1ab
11from polar.kit.address import Address, AddressType 1ab
12from polar.kit.db.models import RecordModel 1ab
13from polar.kit.extensions.sqlalchemy import StringEnum 1ab
14from polar.kit.math import polar_round 1ab
16if TYPE_CHECKING: 16 ↛ 17line 16 didn't jump to line 17 because the condition on line 16 was never true1ab
17 from .organization import Organization
18 from .user import User
21FeeBasisPoints: TypeAlias = int 1ab
22FeeFixedCents: TypeAlias = int 1ab
23Fees: TypeAlias = tuple[FeeBasisPoints, FeeFixedCents] 1ab
26class Account(RecordModel): 1ab
27 class Status(StrEnum): 1ab
28 CREATED = "created" 1ab
29 ONBOARDING_STARTED = "onboarding_started" 1ab
30 UNDER_REVIEW = "under_review" 1ab
31 DENIED = "denied" 1ab
32 ACTIVE = "active" 1ab
34 def get_display_name(self) -> str: 1ab
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" 1ab
45 account_type: Mapped[AccountType] = mapped_column( 1ab
46 StringEnum(AccountType), nullable=False
47 )
49 admin_id: Mapped[UUID] = mapped_column(Uuid, ForeignKey("users.id", use_alter=True)) 1ab
51 stripe_id: Mapped[str | None] = mapped_column( 1ab
52 String(100), nullable=True, default=None
53 )
54 open_collective_slug: Mapped[str | None] = mapped_column( 1ab
55 String(255), nullable=True, default=None
56 )
58 email: Mapped[str | None] = mapped_column(String(254), nullable=True, default=None) 1ab
60 country: Mapped[str] = mapped_column(String(2), nullable=False) 1ab
61 currency: Mapped[str] = mapped_column(String(3)) 1ab
63 is_details_submitted: Mapped[bool] = mapped_column(Boolean, nullable=False) 1ab
64 is_charges_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False) 1ab
65 is_payouts_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False) 1ab
67 processor_fees_applicable: Mapped[bool] = mapped_column( 1ab
68 Boolean, nullable=False, default=True
69 )
70 _platform_fee_percent: Mapped[int | None] = mapped_column( 1ab
71 Integer, name="platform_fee_percent", nullable=True, default=None
72 )
73 _platform_fee_fixed: Mapped[int | None] = mapped_column( 1ab
74 Integer, name="platform_fee_fixed", nullable=True, default=None
75 )
77 business_type: Mapped[str | None] = mapped_column( 1ab
78 String(255), nullable=True, default=None
79 )
81 status: Mapped[Status] = mapped_column( 1ab
82 StringEnum(Status), nullable=False, default=Status.CREATED
83 )
84 next_review_threshold: Mapped[int | None] = mapped_column( 1ab
85 Integer, nullable=True, default=0
86 )
87 campaign_id: Mapped[UUID | None] = mapped_column( 1ab
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) 1ab
96 billing_name: Mapped[str | None] = mapped_column( 1ab
97 String, nullable=True, default=None
98 )
99 billing_address: Mapped[Address | None] = mapped_column(AddressType, nullable=True) 1ab
100 billing_additional_info: Mapped[str | None] = mapped_column( 1ab
101 Text, nullable=True, default=None
102 )
103 billing_notes: Mapped[str | None] = mapped_column(Text, nullable=True, default=None) 1ab
105 @declared_attr 1ab
106 def admin(cls) -> Mapped["User"]: 1ab
107 return relationship("User", lazy="raise", foreign_keys="[Account.admin_id]") 1ab
109 @declared_attr 1ab
110 def users(cls) -> Mapped[list["User"]]: 1ab
111 return relationship( 1ab
112 "User",
113 lazy="raise",
114 back_populates="account",
115 foreign_keys="[User.account_id]",
116 )
118 @declared_attr 1ab
119 def all_organizations(cls) -> Mapped[list["Organization"]]: 1ab
120 return relationship("Organization", lazy="raise", back_populates="account") 1ab
122 @declared_attr 1ab
123 def organizations(cls) -> Mapped[list["Organization"]]: 1ab
124 return relationship( 1ab
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: 1ab
137 return self.status == Account.Status.ACTIVE
139 def is_under_review(self) -> bool: 1ab
140 return self.status == Account.Status.UNDER_REVIEW
142 def is_payout_ready(self) -> bool: 1ab
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]: 1ab
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 1ab
159 def platform_fee(self) -> Fees: 1ab
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: 1ab
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)