Coverage for polar/discount/schemas.py: 75%
129 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-12-05 16:17 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-12-05 16:17 +0000
1import inspect 1a
2from datetime import datetime 1a
3from typing import Annotated, Any, Literal, Self 1a
5from annotated_types import Ge, Le 1a
6from pydantic import ( 1a
7 UUID4,
8 AfterValidator,
9 Discriminator,
10 Field,
11 Tag,
12 TypeAdapter,
13 model_validator,
14)
16from polar.kit.metadata import ( 1a
17 MetadataInputMixin,
18 MetadataOutputMixin,
19)
20from polar.kit.schemas import ( 1a
21 ClassName,
22 EmptyStrToNone,
23 IDSchema,
24 MergeJSONSchema,
25 Schema,
26 SetSchemaReference,
27 TimestampedSchema,
28)
29from polar.models.discount import DiscountDuration, DiscountType 1a
30from polar.organization.schemas import OrganizationID 1a
31from polar.product.schemas import ProductBase 1a
33Name = Annotated[ 1a
34 str,
35 Field(
36 description=(
37 "Name of the discount. "
38 "Will be displayed to the customer when the discount is applied."
39 ),
40 min_length=1,
41 ),
42]
45def _code_validator(value: str | None) -> str | None: 1a
46 if value is None:
47 return None
48 if not value.isalnum():
49 raise ValueError("Code must contain only alphanumeric characters.")
50 if not 3 <= len(value) <= 256:
51 raise ValueError("Code must be between 3 and 256 characters long.")
52 return value
55Code = Annotated[ 1a
56 EmptyStrToNone,
57 AfterValidator(_code_validator),
58 Field(
59 description=(
60 "Code customers can use to apply the discount during checkout. "
61 "Must be between 3 and 256 characters long and "
62 "contain only alphanumeric characters."
63 "If not provided, the discount can only be applied via the API."
64 ),
65 ),
66]
69def _starts_at_ends_at_validator( 1a
70 starts_at: datetime | None, ends_at: datetime | None
71) -> None:
72 if starts_at is not None and ends_at is not None:
73 if starts_at >= ends_at:
74 raise ValueError("starts_at must be before ends_at")
77StartsAt = Annotated[ 1a
78 datetime | None,
79 Field(
80 description="Optional timestamp after which the discount is redeemable.",
81 ),
82]
83EndsAt = Annotated[ 1a
84 datetime | None,
85 Field(
86 description=(
87 "Optional timestamp after which the discount is no longer redeemable."
88 ),
89 ),
90]
91MaxRedemptions = Annotated[ 1a
92 int | None,
93 Field(
94 description="Optional maximum number of times the discount can be redeemed.",
95 ),
96 Ge(1),
97]
98DurationOnceForever = Annotated[ 1a
99 Literal[DiscountDuration.once, DiscountDuration.forever],
100 Field(
101 description=(
102 "For subscriptions, determines if the discount should be applied "
103 "once on the first invoice or forever."
104 )
105 ),
106]
107DurationRepeating = Annotated[ 1a
108 Literal[DiscountDuration.repeating],
109 Field(
110 description=(
111 "For subscriptions, the discount should be applied on every invoice "
112 "for a certain number of months, determined by `duration_in_months`."
113 )
114 ),
115]
116DurationInMonths = Annotated[ 1a
117 int,
118 Field(
119 description=inspect.cleandoc("""
120 Number of months the discount should be applied.
122 For this to work on yearly pricing, you should multiply this by 12.
123 For example, to apply the discount for 2 years, set this to 24.
124 """)
125 ),
126 Ge(1),
127 Le(999),
128]
129Amount = Annotated[ 1a
130 int,
131 Field(
132 description="Fixed amount to discount from the invoice total.",
133 ge=0,
134 le=999999999999,
135 ),
136]
137Currency = Annotated[ 1a
138 str,
139 Field(
140 pattern="usd",
141 description="The currency. Currently, only `usd` is supported.",
142 ),
143]
144BasisPoints = Annotated[ 1a
145 int,
146 Field(
147 description=(
148 inspect.cleandoc("""
149 Discount percentage in basis points.
151 A basis point is 1/100th of a percent.
152 For example, to create a 25.5% discount, set this to 2550.
153 """)
154 ),
155 ge=1,
156 le=10000,
157 ),
158]
159ProductsList = Annotated[ 1a
160 list[UUID4],
161 Field(description="List of product IDs the discount can be applied to."),
162]
165class DiscountCreateBase(MetadataInputMixin, Schema): 1a
166 name: Name 1a
167 type: DiscountType = Field(description="Type of the discount.") 1a
168 code: Code = None 1a
170 starts_at: StartsAt = None 1a
171 ends_at: EndsAt = None 1a
172 max_redemptions: MaxRedemptions = None 1a
174 duration: DiscountDuration 1a
176 products: ProductsList | None = None 1a
178 organization_id: OrganizationID | None = Field( 1a
179 default=None,
180 description=(
181 "The ID of the organization owning the discount. "
182 "**Required unless you use an organization token.**"
183 ),
184 )
186 @model_validator(mode="after") 1a
187 def validate_starts_at_ends_at(self) -> Self: 1a
188 _starts_at_ends_at_validator(self.starts_at, self.ends_at)
189 return self
192class DiscountOnceForeverDurationCreateBase(Schema): 1a
193 duration: DurationOnceForever 1a
196class DiscountRepeatDurationCreateBase(Schema): 1a
197 duration: DurationRepeating 1a
198 duration_in_months: DurationInMonths 1a
201class DiscountFixedCreateBase(Schema): 1a
202 type: Literal[DiscountType.fixed] = DiscountType.fixed 1a
203 amount: Amount 1a
204 currency: Currency = "usd" 1a
207class DiscountPercentageCreateBase(Schema): 1a
208 type: Literal[DiscountType.percentage] = DiscountType.percentage 1a
209 basis_points: BasisPoints 1a
212class DiscountFixedOnceForeverDurationCreate( 1a
213 DiscountCreateBase, DiscountFixedCreateBase, DiscountOnceForeverDurationCreateBase
214):
215 """Schema to create a fixed amount discount that is applied once or forever."""
218class DiscountFixedRepeatDurationCreate( 1a
219 DiscountCreateBase, DiscountFixedCreateBase, DiscountRepeatDurationCreateBase
220):
221 """
222 Schema to create a fixed amount discount that is applied on every invoice
223 for a certain number of months.
224 """
227class DiscountPercentageOnceForeverDurationCreate( 1a
228 DiscountCreateBase,
229 DiscountPercentageCreateBase,
230 DiscountOnceForeverDurationCreateBase,
231):
232 """Schema to create a percentage discount that is applied once or forever."""
235class DiscountPercentageRepeatDurationCreate( 1a
236 DiscountCreateBase, DiscountPercentageCreateBase, DiscountRepeatDurationCreateBase
237):
238 """
239 Schema to create a percentage discount that is applied on every invoice
240 for a certain number of months.
241 """
244class DiscountUpdate(MetadataInputMixin, Schema): 1a
245 """
246 Schema to update a discount.
247 """
249 name: Name | None = None 1a
250 code: Code = None 1a
252 starts_at: StartsAt = None 1a
253 ends_at: EndsAt = None 1a
254 max_redemptions: MaxRedemptions = None 1a
256 duration: DiscountDuration | None = None 1a
257 duration_in_months: DurationInMonths | None = None 1a
259 type: DiscountType | None = None 1a
260 amount: Amount | None = None 1a
261 currency: Currency | None = None 1a
262 basis_points: BasisPoints | None = None 1a
264 products: ProductsList | None = None 1a
266 @model_validator(mode="after") 1a
267 def validate_starts_at_ends_at(self) -> Self: 1a
268 _starts_at_ends_at_validator(self.starts_at, self.ends_at)
269 return self
272class DiscountProduct(ProductBase, MetadataOutputMixin): 1a
273 """A product that a discount can be applied to."""
275 ... 1a
278class DiscountBase(MetadataOutputMixin, IDSchema, TimestampedSchema): 1a
279 name: str = Field( 1a
280 description=(
281 "Name of the discount. "
282 "Will be displayed to the customer when the discount is applied."
283 )
284 )
285 type: DiscountType 1a
286 duration: DiscountDuration 1a
287 code: str | None = Field( 1a
288 description="Code customers can use to apply the discount during checkout."
289 )
291 starts_at: datetime | None = Field( 1a
292 description="Timestamp after which the discount is redeemable."
293 )
294 ends_at: datetime | None = Field( 1a
295 description="Timestamp after which the discount is no longer redeemable."
296 )
297 max_redemptions: int | None = Field( 1a
298 description="Maximum number of times the discount can be redeemed."
299 )
301 redemptions_count: int = Field( 1a
302 description="Number of times the discount has been redeemed."
303 )
305 organization_id: OrganizationID 1a
308class DiscountOnceForeverDurationBase(Schema): 1a
309 duration: Literal[DiscountDuration.once, DiscountDuration.forever] = Field( 1a
310 description=(
311 "For subscriptions, determines if the discount should be applied "
312 "once on the first invoice or forever."
313 )
314 )
317class DiscountRepeatDurationBase(Schema): 1a
318 duration: Literal[DiscountDuration.repeating] = Field( 1a
319 description=(
320 "For subscriptions, the discount should be applied on every invoice "
321 "for a certain number of months, determined by `duration_in_months`."
322 )
323 )
324 duration_in_months: int 1a
327class DiscountFixedBase(Schema): 1a
328 type: Literal[DiscountType.fixed] = DiscountType.fixed 1a
329 amount: int = Field(examples=[1000]) 1a
330 currency: str = Field(examples=["usd"]) 1a
333class DiscountPercentageBase(Schema): 1a
334 type: Literal[DiscountType.percentage] = DiscountType.percentage 1a
335 basis_points: int = Field( 1a
336 examples=[1000],
337 description=(
338 "Discount percentage in basis points. "
339 "A basis point is 1/100th of a percent. "
340 "For example, 1000 basis points equals a 10% discount."
341 ),
342 )
345class DiscountFixedOnceForeverDurationBase( 1a
346 DiscountBase, DiscountFixedBase, DiscountOnceForeverDurationBase
347): ...
350class DiscountFixedRepeatDurationBase( 1a
351 DiscountBase, DiscountFixedBase, DiscountRepeatDurationBase
352): ...
355class DiscountPercentageOnceForeverDurationBase( 1a
356 DiscountBase, DiscountPercentageBase, DiscountOnceForeverDurationBase
357): ...
360class DiscountPercentageRepeatDurationBase( 1a
361 DiscountBase, DiscountPercentageBase, DiscountRepeatDurationBase
362): ...
365class DiscountFullBase(DiscountBase): 1a
366 products: list[DiscountProduct] 1a
369class DiscountFixedOnceForeverDuration( 1a
370 DiscountFullBase, DiscountFixedBase, DiscountOnceForeverDurationBase
371):
372 """Schema for a fixed amount discount that is applied once or forever."""
375class DiscountFixedRepeatDuration( 1a
376 DiscountFullBase, DiscountFixedBase, DiscountRepeatDurationBase
377):
378 """
379 Schema for a fixed amount discount that is applied on every invoice
380 for a certain number of months.
381 """
384class DiscountPercentageOnceForeverDuration( 1a
385 DiscountFullBase, DiscountPercentageBase, DiscountOnceForeverDurationBase
386):
387 """Schema for a percentage discount that is applied once or forever."""
390class DiscountPercentageRepeatDuration( 1a
391 DiscountFullBase, DiscountPercentageBase, DiscountRepeatDurationBase
392):
393 """
394 Schema for a percentage discount that is applied on every invoice
395 for a certain number of months.
396 """
399def get_discriminator_value(v: Any) -> str | None: 1a
400 if isinstance(v, dict):
401 type = v.get("type")
402 duration = v.get("duration")
403 else:
404 type = getattr(v, "type", None)
405 duration = getattr(v, "duration", None)
407 if type is None or duration is None:
408 return None
410 duration_tag = (
411 "once_forever"
412 if duration in {DiscountDuration.once, DiscountDuration.forever}
413 else "repeat"
414 )
415 return f"{type}.{duration_tag}"
418DiscountCreate = Annotated[ 1a
419 Annotated[DiscountFixedOnceForeverDurationCreate, Tag("fixed.once_forever")]
420 | Annotated[DiscountFixedRepeatDurationCreate, Tag("fixed.repeat")]
421 | Annotated[
422 DiscountPercentageOnceForeverDurationCreate, Tag("percentage.once_forever")
423 ]
424 | Annotated[DiscountPercentageRepeatDurationCreate, Tag("percentage.repeat")],
425 Discriminator(get_discriminator_value),
426]
427DiscountMinimal = Annotated[ 1a
428 Annotated[DiscountFixedOnceForeverDurationBase, Tag("fixed.once_forever")]
429 | Annotated[DiscountFixedRepeatDurationBase, Tag("fixed.repeat")]
430 | Annotated[
431 DiscountPercentageOnceForeverDurationBase, Tag("percentage.once_forever")
432 ]
433 | Annotated[DiscountPercentageRepeatDurationBase, Tag("percentage.repeat")],
434 Discriminator(get_discriminator_value),
435]
436Discount = Annotated[ 1a
437 Annotated[DiscountFixedOnceForeverDuration, Tag("fixed.once_forever")]
438 | Annotated[DiscountFixedRepeatDuration, Tag("fixed.repeat")]
439 | Annotated[DiscountPercentageOnceForeverDuration, Tag("percentage.once_forever")]
440 | Annotated[DiscountPercentageRepeatDuration, Tag("percentage.repeat")],
441 Discriminator(get_discriminator_value),
442 SetSchemaReference("Discount"),
443 MergeJSONSchema({"title": "Discount"}),
444 ClassName("Discount"),
445]
446DiscountAdapter: TypeAdapter[Discount] = TypeAdapter(Discount) 1a