Coverage for polar/models/payout.py: 78%
63 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 datetime import datetime
2from enum import StrEnum
3from typing import TYPE_CHECKING
4from uuid import UUID
6from sqlalchemy import TIMESTAMP, ForeignKey, String, UniqueConstraint, Uuid
7from sqlalchemy.orm import Mapped, mapped_column, relationship
8from sqlalchemy.sql.sqltypes import BigInteger
10from polar.enums import AccountType
11from polar.kit.db.models import RecordModel
12from polar.kit.extensions.sqlalchemy.types import StringEnum
14if TYPE_CHECKING: 14 ↛ 15line 14 didn't jump to line 15 because the condition on line 14 was never true
15 from .account import Account
16 from .transaction import Transaction
19class PayoutStatus(StrEnum):
20 pending = "pending"
21 in_transit = "in_transit"
22 succeeded = "succeeded"
24 @classmethod
25 def from_stripe(cls, stripe_status: str) -> "PayoutStatus":
26 if stripe_status == "in_transit":
27 return cls.in_transit
28 if stripe_status == "paid":
29 return cls.succeeded
30 return cls.pending
33class Payout(RecordModel):
34 __tablename__ = "payouts"
35 __table_args__ = (UniqueConstraint("account_id", "invoice_number"),)
37 processor: Mapped[AccountType] = mapped_column(
38 StringEnum(AccountType), nullable=False
39 )
40 """Payment processor used for this payout."""
41 processor_id: Mapped[str | None] = mapped_column(
42 String, nullable=True, index=True, unique=False
43 )
44 """ID of the payout in the payment processor. Might be `None` if not yet created."""
45 status: Mapped[PayoutStatus] = mapped_column(
46 StringEnum(PayoutStatus),
47 nullable=False,
48 index=True,
49 default=PayoutStatus.pending,
50 )
51 """Status of this payout."""
52 paid_at: Mapped[datetime | None] = mapped_column(
53 TIMESTAMP(timezone=True), nullable=True
54 )
55 """Date and time when this payout was paid. Might be `None` if not yet paid."""
56 currency: Mapped[str] = mapped_column(String(3), nullable=False)
57 """Currency of this transaction from Polar's perspective. Should be `usd`."""
58 amount: Mapped[int] = mapped_column(BigInteger, nullable=False)
59 """Amount in cents of this transaction from Polar's perspective."""
60 fees_amount: Mapped[int] = mapped_column(BigInteger, nullable=False)
61 """Fees amount in cents of this transaction from Polar's perspective."""
62 account_currency: Mapped[str] = mapped_column(String(3), nullable=False)
63 """Currency of this transaction from user's account perspective. Might not be `usd`."""
64 account_amount: Mapped[int] = mapped_column(BigInteger, nullable=False)
65 """Amount in cents of this transaction from user's account perspective."""
67 account_id: Mapped[UUID] = mapped_column(
68 Uuid, ForeignKey("accounts.id", ondelete="restrict"), nullable=False
69 )
70 """ID of the `Account` concerned by this payout."""
71 account: Mapped["Account"] = relationship("Account", lazy="raise")
73 invoice_number: Mapped[str] = mapped_column(String, nullable=False)
74 """Reverse invoice number for this payout."""
75 invoice_path: Mapped[str | None] = mapped_column(
76 String, nullable=True, default=None
77 )
78 """
79 Path to the invoice for this payout on the storage bucket.
81 Might be `None` if not yet created.
82 """
84 transaction: Mapped["Transaction"] = relationship(
85 "Transaction",
86 back_populates="payout",
87 lazy="raise",
88 uselist=False,
89 foreign_keys="Transaction.payout_id",
90 )
91 """Transaction associated with this payout."""
93 @property
94 def gross_amount(self) -> int:
95 """Gross amount of this payout in cents."""
96 return self.amount + self.fees_amount
98 @property
99 def fees_transactions(self) -> list["Transaction"]:
100 """List of transactions that are fees for this payout."""
101 return [
102 transaction
103 for transaction in self.transaction.incurred_transactions
104 if transaction.account_id is not None
105 ]
107 @property
108 def is_invoice_generated(self) -> bool:
109 return self.invoice_path is not None