Coverage for polar/benefit/strategies/base/service.py: 66%
42 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
1from typing import Any, Protocol, cast 1a
3from polar.auth.models import AuthSubject 1a
4from polar.exceptions import PolarError, PolarRequestValidationError, ValidationError 1a
5from polar.models import Benefit, Customer, Organization, User 1a
6from polar.postgres import AsyncSession 1a
7from polar.redis import Redis 1a
9from .properties import BenefitGrantProperties, BenefitProperties 1a
12class BenefitServiceError(PolarError): ... 1a
15class BenefitPropertiesValidationError(PolarRequestValidationError): 1a
16 """
17 Benefit properties validation error.
18 """
20 def __init__(self, errors: list[ValidationError]) -> None: 1a
21 errors = [
22 {
23 "loc": ("body", "properties", *error["loc"]),
24 "msg": error["msg"],
25 "type": error["type"],
26 "input": error["input"],
27 }
28 for error in errors
29 ]
30 super().__init__(errors)
33class BenefitRetriableError(BenefitServiceError): 1a
34 """
35 A retriable error occured while granting or revoking the benefit.
36 """
38 defer_seconds: int | None
39 "Number of seconds to wait before retrying. If None, worker defaults will be used." 1a
41 def __init__(self, defer_seconds: int | None = None) -> None: 1a
42 self.defer_seconds = defer_seconds
43 message = "An error occured while granting or revoking the benefit."
44 super().__init__(message)
46 @property 1a
47 def defer_milliseconds(self) -> int | None: 1a
48 """
49 Number of milliseconds to wait before retrying.
50 """
51 return self.defer_seconds * 1000 if self.defer_seconds else None
54class BenefitActionRequiredError(BenefitServiceError): 1a
55 """
56 An action is required from the customer before granting the benefit.
58 Typically, we need the customer to connect an external OAuth account.
59 """
62class BenefitServiceProtocol[BP: BenefitProperties, BGP: BenefitGrantProperties]( 1a
63 Protocol
64):
65 """
66 Protocol that should be implemented by each benefit type service.
68 It allows to implement very customizable and specific logic to fulfill the benefit.
69 """
71 session: AsyncSession 1a
72 redis: Redis 1a
74 should_revoke_individually: bool = False 1a
76 def __init__(self, session: AsyncSession, redis: Redis) -> None: 1a
77 self.session = session
78 self.redis = redis
80 async def grant( 1a
81 self,
82 benefit: Benefit,
83 customer: Customer,
84 grant_properties: BGP,
85 *,
86 update: bool = False,
87 attempt: int = 1,
88 ) -> BGP:
89 """
90 Executes the logic to grant a benefit to a customer.
92 Args:
93 benefit: The Benefit to grant.
94 customer: The customer.
95 grant_properties: Stored properties for this specific benefit and customer.
96 Might be available at this stage if we're updating
97 an already granted benefit.
98 update: Whether we are updating an already granted benefit.
99 attempt: Number of times we attempted to grant the benefit.
100 Useful for the worker to implement retry logic.
102 Returns:
103 A dictionary with data to store for this specific benefit and customer.
104 For example, it can be useful to store external identifiers
105 that may help when updating the grant or revoking it.
106 **Existing properties will be overriden, so be sure to include all the data
107 you want to keep.**
109 Raises:
110 BenefitRetriableError: An temporary error occured,
111 we should be able to retry later.
112 """
113 ...
115 async def cycle( 1a
116 self,
117 benefit: Benefit,
118 customer: Customer,
119 grant_properties: BGP,
120 *,
121 attempt: int = 1,
122 ) -> BGP:
123 """
124 Executes the logic when a subscription is renewed for a new cycle for a granted benefit.
126 Args:
127 benefit: The granted Benefit.
128 customer: The customer.
129 grant_properties: Stored properties for this specific benefit and customer.
130 attempt: Number of times we attempted to grant the benefit.
131 Useful for the worker to implement retry logic.
133 Returns:
134 A dictionary with data to store for this specific benefit and customer.
135 For example, it can be useful to store external identifiers
136 that may help when updating the grant or revoking it.
137 **Existing properties will be overriden, so be sure to include all the data
138 you want to keep.**
140 Raises:
141 BenefitRetriableError: An temporary error occured,
142 we should be able to retry later.
143 """
144 ...
146 async def revoke( 1a
147 self,
148 benefit: Benefit,
149 customer: Customer,
150 grant_properties: BGP,
151 *,
152 attempt: int = 1,
153 ) -> BGP:
154 """
155 Executes the logic to revoke a benefit from a customer.
157 Args:
158 benefit: The Benefit to revoke.
159 customer: The customer.
160 grant_properties: Stored properties for this specific benefit and customer.
161 attempt: Number of times we attempted to revoke the benefit.
162 Useful for the worker to implement retry logic.
164 Returns:
165 A dictionary with data to store for this specific benefit and customer.
166 For example, it can be useful to store external identifiers
167 that may help when updating the grant or revoking it.
168 **Existing properties will be overriden, so be sure to include all the data
169 you want to keep.**
171 Raises:
172 BenefitRetriableError: An temporary error occured,
173 we should be able to retry later.
174 """
175 ...
177 async def requires_update(self, benefit: Benefit, previous_properties: BP) -> bool: 1a
178 """
179 Determines if a benefit update requires to trigger the granting logic again.
181 This method is called whenever a benefit is updated. If it returns `True`, the
182 granting logic will be re-executed again for all the backers, i.e. the `grant`
183 method will be called with the `update` argument set to `True`.
185 Args:
186 benefit: The updated Benefit.
187 previous_properties: The Benefit properties before the update.
188 Use it to check which fields have been updated.
189 """
190 ...
192 async def validate_properties( 1a
193 self, auth_subject: AuthSubject[User | Organization], properties: dict[str, Any]
194 ) -> BP:
195 """
196 Validates the benefit properties before creation.
198 Useful if we need to call external logic to make sure this input is valid.
200 Args:
201 user: The User creating the benefit.
202 properties: The input properties to validate.
204 Returns:
205 The validated Benefit properties.
206 It can be different from the input if needed.
208 Raises:
209 BenefitPropertiesValidationError: The benefit
210 properties are invalid.
211 """
212 ...
214 def _get_properties(self, benefit: Benefit) -> BP: 1a
215 """
216 Returns the Benefit properties.
218 Args:
219 benefit: The benefit.
221 Returns:
222 The benefit properties.
223 """
224 return cast(BP, benefit.properties)