Coverage for functions \ flipdare \ generated \ model \ payment \ payment_model.py: 100%

0 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-05-08 12:22 +1000

1#!/usr/bin/env python 

2# 

3# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved. 

4# 

5# This file is part of Flipdare's proprietary software and contains 

6# confidential and copyrighted material. Unauthorised copying, 

7# modification, distribution, or use of this file is strictly 

8# prohibited without prior written permission from Flipdare Pty Ltd. 

9# 

10# This software includes third-party components licensed under MIT, 

11# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details. 

12# 

13# NOTE: THIS FILE IS AUTO GENERATED. DO NOT EDIT. 

14# 

15# Generated by codegen_models.py 

16# 

17# Modify 'codegen_models.py' 

18# and re-run the script above to update. 

19# 

20# pragma: no cover 

21from __future__ import annotations 

22from datetime import datetime 

23from google.cloud.firestore_v1.transforms import Sentinel 

24from flipdare.core.firestore_field import FirestoreField 

25from flipdare.util.time_util import FirestoreTime 

26from typing import Any, TypedDict, cast, Unpack 

27from enum import StrEnum 

28from pydantic import Field, ConfigDict, TypeAdapter 

29from flipdare.firestore.core.app_base_model import AppBaseModel 

30from flipdare.generated.model.payment.customer_info_model import ( 

31 CustomerInfoModel, 

32 CustomerInfoDict, 

33) 

34from flipdare.generated.model.payment.account_info_model import AccountInfoModel, AccountInfoDict 

35from flipdare.generated.model.payment.audit_info_model import AuditInfoModel, AuditInfoDict 

36from flipdare.generated.model.payment.risk_assessment_model import ( 

37 RiskAssessmentModel, 

38 RiskAssessmentDict, 

39) 

40from flipdare.generated.shared.payment.payment_status import PaymentStatus 

41from flipdare.generated.shared.stripe.stripe_intent_status import StripeIntentStatus 

42from flipdare.generated.model.payment.payment_event_model import ( 

43 PaymentEventModel, 

44 PaymentEventDict, 

45) 

46from flipdare.generated.model.payment.payment_result_model import ( 

47 PaymentResultModel, 

48 PaymentResultDict, 

49) 

50from flipdare.generated.model.payment.payment_schedule_model import ( 

51 PaymentScheduleModel, 

52 PaymentScheduleDict, 

53) 

54from flipdare.app_log import LOG 

55from flipdare.util.time_util import TimeUtil 

56from flipdare.payments.dto.payment_intent_dto import PaymentIntentDTO 

57from flipdare.generated.shared.payment.payment_event_status import PaymentEventStatus 

58from flipdare.generated.shared.model.user.app_fee_type import AppFeeType 

59from flipdare.generated.shared.stripe.stripe_currency_code import StripeCurrencyCode 

60 

61 

62class PaymentKeys(StrEnum): 

63 CREATED_AT = "created_at" 

64 UPDATED_AT = "updated_at" 

65 CUSTOMER_INFO = "customer_info" 

66 ACCOUNT_INFO = "account_info" 

67 AUDIT_INFO = "audit_info" 

68 RISK_ASSESSMENT = "risk_assessment" 

69 STATUS = "status" 

70 INTENT_STATUS = "intent_status" 

71 PAYMENT_INTENT_ID = "payment_intent_id" 

72 PAYMENT_METHOD_ID = "payment_method_id" 

73 AMOUNT = "amount" 

74 LAST_EVENT = "last_event" 

75 RESULT_TOTAL = "result_total" 

76 CAPTURE_BEFORE = "capture_before" 

77 SCHEDULE = "schedule" 

78 STRIPE_DISPUTE_ID = "stripe_dispute_id" 

79 

80 

81# !! IMPORTANT !! 

82# !! 

83# !! this should only be used in the database to query. 

84# !! 

85class PaymentInternalKeys(StrEnum): 

86 CREATED_AT = "created_at" 

87 UPDATED_AT = "updated_at" 

88 

89 

90class PaymentModel(AppBaseModel): 

91 """Represents a payment for a pledge, including charge information and status.""" 

92 

93 model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True) 

94 

95 created_at: FirestoreField = Field( 

96 default_factory=cast("Any", lambda: FirestoreTime.server_timestamp()) 

97 ) 

98 updated_at: FirestoreField = Field( 

99 default_factory=cast("Any", lambda: FirestoreTime.server_timestamp()) 

100 ) 

101 customer_info: CustomerInfoModel 

102 account_info: AccountInfoModel 

103 audit_info: AuditInfoModel 

104 risk_assessment: RiskAssessmentModel 

105 status: PaymentStatus = Field(default=PaymentStatus.PENDING) 

106 intent_status: StripeIntentStatus | None = None 

107 payment_intent_id: str 

108 payment_method_id: str | None = None 

109 amount: int = Field(default=0) 

110 last_event: PaymentEventModel | None = None 

111 result_total: PaymentResultModel = Field(default_factory=lambda: PaymentResultModel()) 

112 capture_before: int | None = None 

113 schedule: PaymentScheduleModel = Field(default_factory=lambda: PaymentScheduleModel()) 

114 stripe_dispute_id: str | None = None 

115 

116 @classmethod 

117 def validate_partial(cls, **data: Unpack[PaymentDict]) -> dict[str, Any]: 

118 """ 

119 Uses Unpack to give you autocomplete and static warnings 

120 if you pass an invalid key or type in your code. 

121 

122 Returns a dict with Firestore field names (aliases) for use with batch.update(). 

123 """ 

124 result: dict[str, Any] = {} 

125 for k, v in data.items(): 

126 if k in cls.__pydantic_fields__: 

127 field_info = cls.__pydantic_fields__[k] 

128 validated_value = cast( 

129 "Any", 

130 TypeAdapter(field_info.annotation).validate_python(v), 

131 ) 

132 # Use alias if defined, otherwise use field name 

133 output_key = field_info.alias or k 

134 result[output_key] = validated_value 

135 return result 

136 

137 # ---- Convenience factories ----------------------------------------- 

138 

139 @classmethod 

140 def create( 

141 cls, 

142 audit_info: AuditInfoModel, 

143 risk_assessment: RiskAssessmentModel, 

144 customer_info: CustomerInfoModel, 

145 account_info: AccountInfoModel, 

146 amount: int, 

147 payment_intent_id: str, 

148 intent_status: StripeIntentStatus | None = None, 

149 payment_method_id: str | None = None, 

150 capture_before: int | None = None, 

151 ) -> PaymentModel: 

152 return PaymentModel( 

153 audit_info=audit_info, 

154 risk_assessment=risk_assessment, 

155 customer_info=customer_info, 

156 account_info=account_info, 

157 amount=amount, 

158 intent_status=intent_status, 

159 capture_before=capture_before, 

160 payment_intent_id=payment_intent_id, 

161 payment_method_id=payment_method_id, 

162 ) 

163 

164 # ---- Convenience predicates ----------------------------------------- 

165 

166 @property 

167 def is_captured(self) -> bool: 

168 return ( 

169 self.last_event is not None and self.last_event.status == PaymentEventStatus.CAPTURED 

170 ) 

171 

172 @property 

173 def is_transferred(self) -> bool: 

174 return ( 

175 self.last_event is not None 

176 and self.last_event.status == PaymentEventStatus.TRANSFERRED 

177 ) 

178 

179 @property 

180 def capture_before_dt(self) -> datetime | None: 

181 # NOTE: stripe store timestamps as epoch ints (hence UTC) 

182 epoch = self.capture_before 

183 if epoch is None: 

184 return None 

185 

186 return TimeUtil.simple_epoch_to_utc_dt(epoch) 

187 

188 @capture_before_dt.setter 

189 def capture_before_dt(self, value: datetime) -> None: 

190 # convert to epoch seconds for stripe 

191 epoch = TimeUtil.dt_to_simple_epoch(value) 

192 self.capture_before = epoch 

193 

194 # ---- Nested Properties ----------------------------------------- 

195 

196 @property 

197 def amount_captured(self) -> int: 

198 return self.result_total.amount_captured 

199 

200 @property 

201 def amount_transferred(self) -> int: 

202 return self.result_total.amount_transferred 

203 

204 @property 

205 def stripe_fee_amount(self) -> int: 

206 return self.result_total.stripe_fee_amount 

207 

208 @property 

209 def app_fee_amount(self) -> int: 

210 return self.result_total.app_fee_amount 

211 

212 @property 

213 def customer_id(self) -> str: 

214 return self.customer_info.customer_id 

215 

216 @property 

217 def customer_currency_code(self) -> StripeCurrencyCode: 

218 return self.customer_info.currency_code 

219 

220 @property 

221 def account_id(self) -> str: 

222 return self.account_info.account_id 

223 

224 @property 

225 def account_currency_code(self) -> StripeCurrencyCode: 

226 return self.account_info.currency_code 

227 

228 @property 

229 def fee_type(self) -> AppFeeType: 

230 return self.account_info.fee_type 

231 

232 @property 

233 def latest_charge_id(self) -> str | None: 

234 if self.last_event is None: 

235 return None 

236 return self.last_event.stripe_charge_id 

237 

238 @property 

239 def event_status(self) -> PaymentEventStatus | None: 

240 if self.last_event is None: 

241 return None 

242 return self.last_event.status 

243 

244 # ---- Processing Logic ----------------------------------------- 

245 

246 def set_capture_on(self, intent_status: StripeIntentStatus, value: float) -> None: 

247 if not intent_status.is_processable: 

248 msg = ( 

249 f"Cannot set capture_on for payment intent {self.payment_intent_id} because " 

250 f"intent status {intent_status} is not in a processable state." 

251 ) 

252 LOG().error(msg) 

253 raise ValueError(msg) 

254 

255 self.intent_status = intent_status 

256 self.schedule.capture_on = value 

257 self.status = PaymentStatus.CAPTURE 

258 

259 def set_transfer_on(self, intent_status: StripeIntentStatus, value: float) -> None: 

260 if not intent_status.is_processable: 

261 msg = ( 

262 f"Cannot set transfer_on for payment intent {self.payment_intent_id} because " 

263 f"intent status {intent_status} is not in a processable state." 

264 ) 

265 LOG().error(msg) 

266 raise ValueError(msg) 

267 

268 self.intent_status = intent_status 

269 self.schedule.transfer_on = value 

270 self.status = PaymentStatus.TRANSFER 

271 

272 def set_refund_on(self, intent_status: StripeIntentStatus, value: float) -> None: 

273 if not intent_status.is_processable: 

274 msg = ( 

275 f"Cannot set refund_on for payment intent {self.payment_intent_id} because " 

276 f"intent status {intent_status} is not in a processable state." 

277 ) 

278 LOG().error(msg) 

279 raise ValueError(msg) 

280 

281 self.intent_status = intent_status 

282 self.schedule.refund_on = value 

283 self.status = PaymentStatus.REFUND 

284 

285 def set_captured( 

286 self, 

287 intent_status: StripeIntentStatus, 

288 amount_captured: int = 0, 

289 stripe_fee_amount: int = 0, 

290 app_fee_amount: int = 0, 

291 ) -> None: 

292 if not intent_status.is_completed: 

293 msg = ( 

294 f"Cannot set captured for payment intent {self.payment_intent_id} because " 

295 f"intent status {intent_status} is not in a completed state." 

296 ) 

297 LOG().error(msg) 

298 raise ValueError(msg) 

299 

300 self.intent_status = intent_status 

301 result = self.result_total 

302 result.amount_captured += amount_captured 

303 result.stripe_fee_amount += stripe_fee_amount 

304 result.app_fee_amount += app_fee_amount 

305 

306 self.status = PaymentStatus.COMPLETE 

307 

308 def update_with_intent(self, intent: PaymentIntentDTO) -> None: 

309 payment_method_id = intent.payment_method_id 

310 if payment_method_id is not None: 

311 self.payment_method_id = payment_method_id 

312 

313 capture_before = intent.capture_before 

314 if capture_before is not None: 

315 epoch = TimeUtil.dt_to_simple_epoch(capture_before) 

316 self.capture_before = epoch 

317 

318 intent_status = intent.intent_status 

319 if intent_status != self.intent_status: 

320 self.intent_status = intent_status 

321 

322 amount_captured = intent.amount_captured 

323 if amount_captured is not None and amount_captured != self.amount_captured: 

324 self.result_total.amount_captured += amount_captured 

325 

326 app_fee_amount = intent.app_fee_amount 

327 if app_fee_amount is not None and app_fee_amount != self.app_fee_amount: 

328 self.result_total.app_fee_amount += app_fee_amount 

329 

330 stripe_fee_amount = intent.stripe_fee_amount 

331 if stripe_fee_amount is not None and stripe_fee_amount != self.stripe_fee_amount: 

332 self.result_total.stripe_fee_amount += stripe_fee_amount 

333 

334 

335PAYMENT_FIELD_NAMES: list[str] = list(PaymentModel.model_fields.keys()) 

336 

337 

338class PaymentDict(TypedDict, total=False): 

339 created_at: Sentinel | datetime | str 

340 updated_at: Sentinel | datetime | str 

341 customer_info: CustomerInfoDict 

342 account_info: AccountInfoDict 

343 audit_info: AuditInfoDict 

344 risk_assessment: RiskAssessmentDict 

345 status: PaymentStatus | None 

346 intent_status: StripeIntentStatus | None 

347 payment_intent_id: str 

348 payment_method_id: str | None 

349 amount: int | None 

350 last_event: PaymentEventDict | None 

351 result_total: PaymentResultDict 

352 capture_before: int | None 

353 schedule: PaymentScheduleDict 

354 stripe_dispute_id: str | None