Coverage for polar/benefit/strategies/base/service.py: 66%

42 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-12-05 17:15 +0000

1from typing import Any, Protocol, cast 1a

2 

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

8 

9from .properties import BenefitGrantProperties, BenefitProperties 1a

10 

11 

12class BenefitServiceError(PolarError): ... 1a

13 

14 

15class BenefitPropertiesValidationError(PolarRequestValidationError): 1a

16 """ 

17 Benefit properties validation error. 

18 """ 

19 

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) 

31 

32 

33class BenefitRetriableError(BenefitServiceError): 1a

34 """ 

35 A retriable error occured while granting or revoking the benefit. 

36 """ 

37 

38 defer_seconds: int | None 

39 "Number of seconds to wait before retrying. If None, worker defaults will be used." 1a

40 

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) 

45 

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 

52 

53 

54class BenefitActionRequiredError(BenefitServiceError): 1a

55 """ 

56 An action is required from the customer before granting the benefit. 

57 

58 Typically, we need the customer to connect an external OAuth account. 

59 """ 

60 

61 

62class BenefitServiceProtocol[BP: BenefitProperties, BGP: BenefitGrantProperties]( 1a

63 Protocol 

64): 

65 """ 

66 Protocol that should be implemented by each benefit type service. 

67 

68 It allows to implement very customizable and specific logic to fulfill the benefit. 

69 """ 

70 

71 session: AsyncSession 1a

72 redis: Redis 1a

73 

74 should_revoke_individually: bool = False 1a

75 

76 def __init__(self, session: AsyncSession, redis: Redis) -> None: 1a

77 self.session = session 

78 self.redis = redis 

79 

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. 

91 

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. 

101 

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.** 

108 

109 Raises: 

110 BenefitRetriableError: An temporary error occured, 

111 we should be able to retry later. 

112 """ 

113 ... 

114 

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. 

125 

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. 

132 

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.** 

139 

140 Raises: 

141 BenefitRetriableError: An temporary error occured, 

142 we should be able to retry later. 

143 """ 

144 ... 

145 

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. 

156 

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. 

163 

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.** 

170 

171 Raises: 

172 BenefitRetriableError: An temporary error occured, 

173 we should be able to retry later. 

174 """ 

175 ... 

176 

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. 

180 

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`. 

184 

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 ... 

191 

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. 

197 

198 Useful if we need to call external logic to make sure this input is valid. 

199 

200 Args: 

201 user: The User creating the benefit. 

202 properties: The input properties to validate. 

203 

204 Returns: 

205 The validated Benefit properties. 

206 It can be different from the input if needed. 

207 

208 Raises: 

209 BenefitPropertiesValidationError: The benefit 

210 properties are invalid. 

211 """ 

212 ... 

213 

214 def _get_properties(self, benefit: Benefit) -> BP: 1a

215 """ 

216 Returns the Benefit properties. 

217 

218 Args: 

219 benefit: The benefit. 

220 

221 Returns: 

222 The benefit properties. 

223 """ 

224 return cast(BP, benefit.properties)