Coverage for polar/models/customer_seat.py: 77%
48 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, Any
4from uuid import UUID
6from sqlalchemy import TIMESTAMP, CheckConstraint, ForeignKey, String, Uuid
7from sqlalchemy.dialects.postgresql import JSONB
8from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship
10from polar.kit.db.models import RecordModel
11from polar.product.guard import is_metered_price
13if TYPE_CHECKING: 13 ↛ 14line 13 didn't jump to line 14 because the condition on line 13 was never true
14 from .customer import Customer
15 from .order import Order
16 from .subscription import Subscription
19class SeatStatus(StrEnum):
20 pending = "pending"
21 claimed = "claimed"
22 revoked = "revoked"
25class CustomerSeat(RecordModel):
26 """
27 Represents a seat that can be assigned to a customer for access to benefits. It's
28 mainly used for the seat-based billing.
30 Seats can be associated with either:
31 - A recurring subscription (subscription_id) for ongoing seat-based billing
32 - A one-time order (order_id) for perpetual seat-based purchases
34 Lifecycle:
35 1. pending: Seat created but not yet assigned/claimed by an end customer
36 2. claimed: Customer has accepted the invitation and benefits are granted
37 3. revoked: Seat has been revoked, benefits removed, can be reassigned
39 Key constraints:
40 - Exactly one of subscription_id OR order_id must be set (enforced by seat_source_check)
41 - For subscriptions: seats are tied to billing cycle, revoked when subscription ends
42 - For orders: seats are perpetual, never expire once claimed
43 """
45 __tablename__ = "customer_seats"
46 __table_args__ = (
47 CheckConstraint(
48 "(subscription_id IS NOT NULL AND order_id IS NULL) OR "
49 "(subscription_id IS NULL AND order_id IS NOT NULL)",
50 name="seat_source_check",
51 ),
52 )
54 subscription_id: Mapped[UUID | None] = mapped_column(
55 Uuid,
56 ForeignKey("subscriptions.id", ondelete="cascade"),
57 nullable=True,
58 index=True,
59 )
61 order_id: Mapped[UUID | None] = mapped_column(
62 Uuid,
63 ForeignKey("orders.id", ondelete="cascade"),
64 nullable=True,
65 index=True,
66 )
68 status: Mapped[SeatStatus] = mapped_column(
69 String, nullable=False, default=SeatStatus.pending
70 )
72 customer_id: Mapped[UUID | None] = mapped_column(
73 Uuid,
74 ForeignKey("customers.id", ondelete="set null"),
75 nullable=True,
76 index=True,
77 )
79 invitation_token: Mapped[str | None] = mapped_column(
80 String, nullable=True, default=None, index=True
81 )
83 invitation_token_expires_at: Mapped[datetime | None] = mapped_column(
84 TIMESTAMP(timezone=True), nullable=True, default=None
85 )
87 claimed_at: Mapped[datetime | None] = mapped_column(
88 TIMESTAMP(timezone=True), nullable=True, default=None
89 )
91 revoked_at: Mapped[datetime | None] = mapped_column(
92 TIMESTAMP(timezone=True), nullable=True, default=None
93 )
95 seat_metadata: Mapped[dict[str, Any] | None] = mapped_column(
96 "metadata", JSONB, nullable=True, default=None
97 )
99 @declared_attr
100 def subscription(cls) -> Mapped["Subscription | None"]:
101 return relationship("Subscription", lazy="raise")
103 @declared_attr
104 def order(cls) -> Mapped["Order | None"]:
105 return relationship("Order", lazy="raise")
107 @declared_attr
108 def customer(cls) -> Mapped["Customer | None"]:
109 return relationship("Customer", lazy="raise")
111 def is_pending(self) -> bool:
112 return self.status == SeatStatus.pending
114 def is_claimed(self) -> bool:
115 return self.status == SeatStatus.claimed
117 def is_revoked(self) -> bool:
118 return self.status == SeatStatus.revoked
120 def has_metered_pricing(self) -> bool:
121 if not self.subscription:
122 return False
124 return any(
125 is_metered_price(spp.product_price)
126 for spp in self.subscription.subscription_product_prices
127 )