Coverage for polar/checkout_link/schemas.py: 98%
53 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 typing import Annotated, Literal
3from pydantic import UUID4, AliasPath, Field, HttpUrl, computed_field
4from pydantic.json_schema import SkipJsonSchema
6from polar.config import settings
7from polar.discount.schemas import DiscountMinimal
8from polar.enums import PaymentProcessor
9from polar.kit.metadata import (
10 MetadataInputMixin,
11 MetadataOutputMixin,
12)
13from polar.kit.schemas import (
14 IDSchema,
15 MergeJSONSchema,
16 Schema,
17 SetSchemaReference,
18 TimestampedSchema,
19)
20from polar.kit.trial import TrialConfigurationInputMixin, TrialConfigurationOutputMixin
21from polar.organization.schemas import OrganizationID
22from polar.product.schemas import (
23 BenefitPublicList,
24 ProductBase,
25 ProductMediaList,
26 ProductPrice,
27 ProductPriceList,
28)
30SuccessURL = Annotated[
31 HttpUrl | None,
32 Field(
33 description=(
34 "URL where the customer will be redirected after a successful payment."
35 "You can add the `checkout_id={CHECKOUT_ID}` query parameter "
36 "to retrieve the checkout session id."
37 )
38 ),
39]
41_allow_discount_codes_description = (
42 "Whether to allow the customer to apply discount codes. "
43 "If you apply a discount through `discount_id`, it'll still be applied, "
44 "but the customer won't be able to change it."
45)
46_require_billing_address_description = (
47 "Whether to require the customer to fill their full billing address, instead of "
48 "just the country. "
49 "Customers in the US will always be required to fill their full address, "
50 "regardless of this setting."
51)
52_discount_id_description = (
53 "ID of the discount to apply to the checkout. "
54 "If the discount is not applicable anymore when opening the checkout link, "
55 "it'll be ignored."
56)
59class CheckoutLinkCreateBase(TrialConfigurationInputMixin, MetadataInputMixin, Schema):
60 payment_processor: Literal[PaymentProcessor.stripe] = Field(
61 description="Payment processor to use. Currently only Stripe is supported."
62 )
63 label: str | None = Field(
64 description="Optional label to distinguish links internally", default=None
65 )
66 allow_discount_codes: bool = Field(
67 default=True, description=_allow_discount_codes_description
68 )
69 require_billing_address: bool = Field(
70 default=False, description=_require_billing_address_description
71 )
72 discount_id: UUID4 | None = Field(
73 default=None, description=_discount_id_description
74 )
75 success_url: SuccessURL = None
78class CheckoutLinkCreateProductPrice(CheckoutLinkCreateBase):
79 """
80 Schema to create a new checkout link from a a single product price.
82 **Deprecated**: Use `CheckoutLinkCreateProducts` instead.
83 """
85 product_price_id: UUID4
88class CheckoutLinkCreateProduct(CheckoutLinkCreateBase):
89 """
90 Schema to create a new checkout link from a a single product.
92 **Deprecated**: Use `CheckoutLinkCreateProducts` instead.
93 """
95 product_id: UUID4
98class CheckoutLinkCreateProducts(CheckoutLinkCreateBase):
99 """Schema to create a new checkout link."""
101 products: list[UUID4] = Field(
102 description="List of products that will be available to select at checkout.",
103 min_length=1,
104 )
107CheckoutLinkCreate = Annotated[
108 CheckoutLinkCreateProductPrice
109 | CheckoutLinkCreateProduct
110 | CheckoutLinkCreateProducts,
111 SetSchemaReference("CheckoutLinkCreate"),
112]
115class CheckoutLinkUpdate(MetadataInputMixin, TrialConfigurationInputMixin):
116 """Schema to update an existing checkout link."""
118 products: list[UUID4] | None = Field(
119 default=None,
120 description="List of products that will be available to select at checkout.",
121 min_length=1,
122 )
123 label: str | None = None
124 allow_discount_codes: bool | None = Field(
125 default=None, description=_allow_discount_codes_description
126 )
127 require_billing_address: bool | None = Field(
128 default=None, description=_require_billing_address_description
129 )
130 discount_id: UUID4 | None = Field(
131 default=None, description=_discount_id_description
132 )
133 success_url: SuccessURL = None
136class CheckoutLinkBase(
137 MetadataOutputMixin, TrialConfigurationOutputMixin, TimestampedSchema, IDSchema
138):
139 payment_processor: PaymentProcessor = Field(description="Payment processor used.")
140 client_secret: str = Field(
141 description="Client secret used to access the checkout link."
142 )
143 success_url: str | None = Field(
144 description=(
145 "URL where the customer will be redirected after a successful payment."
146 )
147 )
148 label: str | None = Field(
149 description="Optional label to distinguish links internally"
150 )
151 allow_discount_codes: bool = Field(description=_allow_discount_codes_description)
152 require_billing_address: bool = Field(
153 description=_require_billing_address_description
154 )
155 discount_id: UUID4 | None = Field(description=_discount_id_description)
156 organization_id: OrganizationID
158 @computed_field # type: ignore[prop-decorator]
159 @property
160 def url(self) -> str:
161 return settings.CHECKOUT_BASE_URL.format(client_secret=self.client_secret)
164class CheckoutLinkProduct(ProductBase, MetadataOutputMixin):
165 """Product data for a checkout link."""
167 prices: ProductPriceList
168 benefits: BenefitPublicList
169 medias: ProductMediaList
172CheckoutLinkDiscount = Annotated[
173 DiscountMinimal, MergeJSONSchema({"title": "CheckoutLinkDiscount"})
174]
177class CheckoutLink(CheckoutLinkBase):
178 """Checkout link data."""
180 products: list[CheckoutLinkProduct]
181 discount: CheckoutLinkDiscount | None
183 # Deprecated fields for backward compatibility
184 product_id: SkipJsonSchema[UUID4] = Field(
185 validation_alias=AliasPath("products", 0, "id"),
186 deprecated="Use `products` instead.",
187 )
188 product_price_id: SkipJsonSchema[UUID4] = Field(
189 validation_alias=AliasPath("products", 0, "prices", 0, "id"),
190 deprecated="Use `products` instead.",
191 )
192 product: SkipJsonSchema[CheckoutLinkProduct] = Field(
193 validation_alias=AliasPath("products", 0),
194 deprecated="Use `products` instead.",
195 )
196 product_price: SkipJsonSchema[ProductPrice] = Field(
197 validation_alias=AliasPath("products", 0, "prices", 0),
198 deprecated="Use `products` instead.",
199 )