Coverage for polar/models/order_item.py: 56%
54 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-12-05 17:15 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-12-05 17:15 +0000
1from datetime import datetime 1ab
2from typing import TYPE_CHECKING, Self 1ab
3from uuid import UUID 1ab
5from babel.dates import format_date 1ab
6from sqlalchemy import Boolean, ForeignKey, Integer, String, Uuid 1ab
7from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy 1ab
8from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship 1ab
10from polar.kit.db.models import RecordModel 1ab
11from polar.models.product_price import ( 1ab
12 LegacyRecurringProductPriceCustom,
13 LegacyRecurringProductPriceFixed,
14 LegacyRecurringProductPriceFree,
15 ProductPrice,
16 ProductPriceCustom,
17 ProductPriceFixed,
18 ProductPriceFree,
19 ProductPriceSeatUnit,
20)
22if TYPE_CHECKING: 22 ↛ 23line 22 didn't jump to line 23 because the condition on line 22 was never true1ab
23 from polar.models import Order, Product, Wallet
26class OrderItem(RecordModel): 1ab
27 __tablename__ = "order_items" 1ab
29 label: Mapped[str] = mapped_column(String, nullable=False) 1ab
30 amount: Mapped[int] = mapped_column(Integer, nullable=False) 1ab
31 tax_amount: Mapped[int] = mapped_column(Integer, nullable=False) 1ab
32 proration: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) 1ab
33 order_id: Mapped[UUID] = mapped_column( 1ab
34 Uuid, ForeignKey("orders.id", ondelete="cascade")
35 )
36 product_price_id: Mapped[UUID | None] = mapped_column( 1ab
37 Uuid, ForeignKey("product_prices.id", ondelete="restrict"), nullable=True
38 )
40 @declared_attr 1ab
41 def product_price(cls) -> Mapped["ProductPrice | None"]: 1ab
42 return relationship("ProductPrice", lazy="raise_on_sql") 1ab
44 @declared_attr 1ab
45 def order(cls) -> Mapped["Order"]: 1ab
46 return relationship("Order", lazy="raise_on_sql", back_populates="items") 1ab
48 product: AssociationProxy["Product"] = association_proxy("product_price", "product") 1ab
50 @property 1ab
51 def total_amount(self) -> int: 1ab
52 return self.amount + self.tax_amount
54 @property 1ab
55 def discountable(self) -> bool: 1ab
56 # Simple logic for now: only non-prorated items are discountable
57 # But could be expanded in the future by having a dedicated column
58 return not self.proration
60 @classmethod 1ab
61 def from_price( 1ab
62 cls,
63 price: ProductPrice,
64 tax_amount: int,
65 amount: int | None = None,
66 seats: int | None = None,
67 ) -> Self:
68 if isinstance(price, ProductPriceFixed | LegacyRecurringProductPriceFixed):
69 amount = price.price_amount
70 elif isinstance(price, ProductPriceCustom | LegacyRecurringProductPriceCustom):
71 assert amount is not None, "amount must be provided for custom prices"
72 elif isinstance(price, ProductPriceFree | LegacyRecurringProductPriceFree):
73 amount = 0
74 elif isinstance(price, ProductPriceSeatUnit):
75 assert seats is not None, "seats must be provided for seat-based prices"
76 amount = price.calculate_amount(seats)
77 return cls(
78 label=price.product.name,
79 amount=amount,
80 tax_amount=tax_amount,
81 proration=False,
82 product_price=price,
83 )
85 @classmethod 1ab
86 def from_trial(cls, product: "Product", start: datetime, end: datetime) -> Self: 1ab
87 formatted_start = format_date(start.date(), locale="en_US")
88 formatted_end = format_date(end.date(), locale="en_US")
89 label = f"Trial period for {product.name} ({formatted_start} - {formatted_end})"
90 return cls(label=label, amount=0, tax_amount=0, proration=False)
92 @classmethod 1ab
93 def from_wallet(cls, wallet: "Wallet", amount: int) -> Self: 1ab
94 label = f"Wallet Top-Up for {wallet.organization.name}"
95 return cls(label=label, amount=amount, tax_amount=0, proration=False)