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

1from typing import Annotated, Literal 

2 

3from pydantic import UUID4, AliasPath, Field, HttpUrl, computed_field 

4from pydantic.json_schema import SkipJsonSchema 

5 

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) 

29 

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] 

40 

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) 

57 

58 

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 

76 

77 

78class CheckoutLinkCreateProductPrice(CheckoutLinkCreateBase): 

79 """ 

80 Schema to create a new checkout link from a a single product price. 

81 

82 **Deprecated**: Use `CheckoutLinkCreateProducts` instead. 

83 """ 

84 

85 product_price_id: UUID4 

86 

87 

88class CheckoutLinkCreateProduct(CheckoutLinkCreateBase): 

89 """ 

90 Schema to create a new checkout link from a a single product. 

91 

92 **Deprecated**: Use `CheckoutLinkCreateProducts` instead. 

93 """ 

94 

95 product_id: UUID4 

96 

97 

98class CheckoutLinkCreateProducts(CheckoutLinkCreateBase): 

99 """Schema to create a new checkout link.""" 

100 

101 products: list[UUID4] = Field( 

102 description="List of products that will be available to select at checkout.", 

103 min_length=1, 

104 ) 

105 

106 

107CheckoutLinkCreate = Annotated[ 

108 CheckoutLinkCreateProductPrice 

109 | CheckoutLinkCreateProduct 

110 | CheckoutLinkCreateProducts, 

111 SetSchemaReference("CheckoutLinkCreate"), 

112] 

113 

114 

115class CheckoutLinkUpdate(MetadataInputMixin, TrialConfigurationInputMixin): 

116 """Schema to update an existing checkout link.""" 

117 

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 

134 

135 

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 

157 

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) 

162 

163 

164class CheckoutLinkProduct(ProductBase, MetadataOutputMixin): 

165 """Product data for a checkout link.""" 

166 

167 prices: ProductPriceList 

168 benefits: BenefitPublicList 

169 medias: ProductMediaList 

170 

171 

172CheckoutLinkDiscount = Annotated[ 

173 DiscountMinimal, MergeJSONSchema({"title": "CheckoutLinkDiscount"}) 

174] 

175 

176 

177class CheckoutLink(CheckoutLinkBase): 

178 """Checkout link data.""" 

179 

180 products: list[CheckoutLinkProduct] 

181 discount: CheckoutLinkDiscount | None 

182 

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 )