Coverage for functions \ flipdare \ service \ payments \ _payment_charge_handler.py: 9%

592 statements  

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

1#!/usr/bin/env python 

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

3# 

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

5# confidential and copyrighted material. Unauthorised copying, 

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

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

8# 

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

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

11# 

12 

13from __future__ import annotations 

14from typing import TYPE_CHECKING, Any, Literal 

15from firebase_functions import https_fn 

16from flipdare.app_log import LOG 

17from flipdare.constants import IS_DEBUG, PAYMENT_MAX_INFO_RETRIES, PAYMENT_MAX_RETRIES 

18from flipdare.generated import ( 

19 PaymentCreateResponseSchema, 

20 PledgeModel, 

21 PaymentEventStatus, 

22 AppPaymentErrorCode, 

23 PaymentConfirmResponseSchema, 

24 ErrorSchema, 

25 AuditInfoModel, 

26 PaymentEventModel, 

27 PaymentModel, 

28) 

29from flipdare.generated.model.payment.payment_result_model import PaymentResultModel 

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

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

32from flipdare.payments.data.app_payment_context import AppPaymentContext 

33from flipdare.payments.data.payment_event_context import PaymentEventContext 

34from flipdare.payments.data.payment_schedule import PaymentSchedule 

35from flipdare.payments.data.payment_validator import PaymentValidator 

36from flipdare.payments.dto.payment_intent_dto import PaymentIntentDTO 

37from flipdare.payments.payment_types import AccountRiskInfo 

38from flipdare.payments.app_stripe_proxy import AppStripeProxy 

39from flipdare.error import ( 

40 AppError, 

41 StripeErrorContext, 

42) 

43from flipdare.result.app_result import AppResult 

44from flipdare.result.job_result import JobResult 

45from flipdare.service._error_mixin import ErrorMixin 

46from flipdare.request import ( 

47 AppRequest, 

48 PaymentConfirmRequestAdapter, 

49 PaymentCreateRequestAdapter, 

50) 

51from flipdare.service._service_provider import ServiceProvider 

52from flipdare.service._user_mixin import UserMixin 

53from flipdare.service.payments._base_payment_handler import BasePaymentHandler 

54from flipdare.wrapper.payment.payment_event_wrapper import PaymentEventWrapper 

55from flipdare.wrapper.payment.pledge_wrapper import PledgeWrapper 

56from flipdare.wrapper.user_wrapper import UserWrapper 

57from flipdare.payments.core.stripe_guard import StripeGuard 

58from flipdare.util.slug_coder import SlugCoder 

59 

60if TYPE_CHECKING: 

61 from flipdare.manager.service_manager import ServiceManager 

62 from flipdare.service.payments._payment_account_handler import PaymentAccountHandler 

63 from flipdare.manager.backend_manager import BackendManager 

64 from flipdare.manager.db_manager import DbManager 

65 

66__all__ = ["PaymentChargeHandler"] 

67 

68 

69class PaymentChargeHandler(BasePaymentHandler, ErrorMixin, UserMixin, ServiceProvider): 

70 def __init__( 

71 self, 

72 proxy: AppStripeProxy, 

73 account_handler: PaymentAccountHandler, 

74 db_manager: DbManager | None = None, 

75 backend_manager: BackendManager | None = None, 

76 service_manager: ServiceManager | None = None, 

77 ) -> None: 

78 self.proxy = proxy 

79 self.account_handler = account_handler 

80 

81 super().__init__( 

82 db_manager=db_manager, 

83 backend_manager=backend_manager, 

84 service_manager=service_manager, 

85 ) 

86 

87 # ======================================================================== 

88 # WEB METHODS 

89 # ======================================================================== 

90 

91 def callable_create_charge( 

92 self, 

93 req: https_fn.CallableRequest[Any], 

94 ) -> PaymentCreateResponseSchema | ErrorSchema: 

95 request = AppRequest.callable(req) 

96 endpoint = request.endpoint or "(internal)create_charge" 

97 adapter: PaymentCreateRequestAdapter | None = None 

98 try: 

99 adapter = PaymentCreateRequestAdapter.from_callable(req) 

100 adapter.validate() 

101 except AppError as e: 

102 msg = f"Create Charge Request validation error: {e}\n\tRequest: {request!s}" 

103 LOG().error(msg) 

104 error = StripeErrorContext.from_code( 

105 endpoint=endpoint, 

106 error_code=AppPaymentErrorCode.INVALID_REQUEST, 

107 cause=msg, 

108 error=e, 

109 ) 

110 return error.to_dict() 

111 

112 customer_uid = adapter.customer_uid 

113 cust_user: UserWrapper | None = None 

114 try: 

115 cust_user = self.get_user_by_id(endpoint=endpoint, uid=customer_uid) 

116 except Exception as e: 

117 msg = f"Failed to retrieve from user for uid={customer_uid}\n\tError:{e}" 

118 LOG().error(msg) 

119 error = StripeErrorContext.from_code( 

120 endpoint=endpoint, 

121 error_code=AppPaymentErrorCode.INVALID_USER, 

122 cause=msg, 

123 error=e, 

124 ) 

125 return error.to_dict() 

126 

127 account_uid = adapter.account_uid 

128 risk_info: AccountRiskInfo | None = None 

129 try: 

130 risk_info = self.get_account_risk(endpoint=endpoint, account_uid=account_uid) 

131 except Exception as e: 

132 msg = f"Failed to retrieve account risk info for uid={account_uid}\n\tError:{e}" 

133 LOG().error(msg) 

134 error = StripeErrorContext.from_code( 

135 endpoint=endpoint, 

136 error_code=AppPaymentErrorCode.INVALID_USER, 

137 cause=msg, 

138 error=e, 

139 ) 

140 return error.to_dict() 

141 

142 customer_id = adapter.customer_id 

143 currency_code = adapter.currency_code 

144 email = adapter.email 

145 name = adapter.name 

146 

147 audit_info = AuditInfoModel.create( 

148 endpoint=endpoint, 

149 uid=customer_uid, 

150 ip_address=adapter.ip_address, 

151 device_fingerprint=adapter.device_fingerprint, 

152 ) 

153 

154 if customer_id is None: 

155 try: 

156 customer_id = self.account_handler.create_customer( 

157 endpoint=endpoint, 

158 user=cust_user, 

159 alt_currency_code=currency_code, 

160 name=name, 

161 email=email, 

162 ) 

163 except Exception as e: 

164 msg = f"Failed to create stripe customer for payment creation for user {customer_uid}\n\tError:{e}" 

165 LOG().error(msg) 

166 error = StripeErrorContext.from_code( 

167 endpoint=endpoint, 

168 error_code=AppPaymentErrorCode.INVALID_USER, 

169 cause=msg, 

170 error=e, 

171 ) 

172 return error.to_dict() 

173 

174 pledge_amount = adapter.pledge_amount 

175 dare_id = adapter.dare_id 

176 try: 

177 ctx = self._build_payment_context( 

178 audit_info=audit_info, 

179 risk_info=risk_info, 

180 cust_user=cust_user, 

181 dare_id=dare_id, 

182 amount=pledge_amount, 

183 ) 

184 payment_response = self.proxy.create_payment_intent(endpoint, ctx) 

185 payment_intent_id = payment_response["payment_intent_id"] 

186 

187 self._create_pledge_in_db( 

188 ctx=ctx, 

189 payment_intent_id=payment_intent_id, 

190 ) 

191 return payment_response 

192 

193 except Exception as e: 

194 msg = f"Failed to create charge for user {customer_uid}\n\tError:{e}" 

195 LOG().error(msg) 

196 error = StripeErrorContext.from_code( 

197 endpoint=endpoint, 

198 error_code=AppPaymentErrorCode.PAYMENT_CREATE_FAILED, 

199 cause=msg, 

200 error=e, 

201 ) 

202 return error.to_dict() 

203 

204 def callable_confirm_charge( 

205 self, 

206 req: https_fn.CallableRequest[Any], 

207 ) -> PaymentConfirmResponseSchema | ErrorSchema: 

208 # save the payment method id so it can be charged later 

209 # when the user confirms the pledge in the app 

210 # NOTE: we no longer cancel the intent immediately. 

211 # NOTE: this is because the charge may need to be re-authorized later if the 

212 # NOTE: dare/voting takes longer than the payment intent authorization hold period. 

213 # NOTE: instead, we add a charge to the capture_charge_db 

214 # NOTE: so we can re-authorize, delay capture etc. 

215 

216 request = AppRequest.callable(req) 

217 endpoint = request.endpoint or "(internal)authorize_charge" 

218 

219 adapter: PaymentConfirmRequestAdapter | None = None 

220 try: 

221 adapter = PaymentConfirmRequestAdapter.from_callable(req) 

222 adapter.validate() 

223 except AppError as e: 

224 msg = f"Authorize Charge Request validation error: {e}\n\tRequest: {request!s}" 

225 LOG().error(msg) 

226 error = StripeErrorContext.from_code( 

227 endpoint=endpoint, 

228 error_code=AppPaymentErrorCode.INVALID_REQUEST, 

229 cause=msg, 

230 error=e, 

231 ) 

232 return error.to_dict() 

233 

234 customer_uid = adapter.customer_uid 

235 payment_intent_id = adapter.payment_intent_id 

236 payment_method_id: str | None = None 

237 debug_str = f"(payment_intent_id {payment_intent_id} for user {customer_uid})" 

238 

239 # get the payment method id from the payment_intent_id 

240 try: 

241 intent = self.proxy.get_payment_intent(endpoint, payment_intent_id) 

242 payment_method_id = intent.payment_method_id 

243 except Exception as e: 

244 msg = f"Failed to retrieve payment intent {debug_str}\n\tError:{e}" 

245 LOG().error(msg) 

246 error = StripeErrorContext.from_code( 

247 endpoint=endpoint, 

248 error_code=AppPaymentErrorCode.CHARGE_AUTHORIZE_FAILED, 

249 cause=msg, 

250 error=e, 

251 ) 

252 return error.to_dict() 

253 

254 if payment_method_id is None: 

255 msg = f"Payment intent {debug_str} has no payment method id, cannot authorize charge." 

256 LOG().error(msg) 

257 error = StripeErrorContext.from_code( 

258 endpoint=endpoint, 

259 error_code=AppPaymentErrorCode.CHARGE_AUTHORIZE_FAILED, 

260 cause=msg, 

261 ) 

262 return error.to_dict() 

263 

264 LOG().debug(f"Authorizing charge {debug_str} with payment method {payment_method_id}") 

265 pledge_amount = adapter.pledge_amount 

266 pledge_id = adapter.pledge_id 

267 

268 try: 

269 self._update_payment_method( 

270 endpoint=endpoint, 

271 pledge_id=pledge_id, 

272 payment_method_id=payment_method_id, 

273 ) 

274 return PaymentConfirmResponseSchema( 

275 uid=customer_uid, 

276 pledge_amount=pledge_amount, 

277 ) 

278 except Exception as e: 

279 msg = f"Failed to update charge {debug_str} and payment method {payment_method_id}\n\tError:{e}" 

280 LOG().error(msg) 

281 error = StripeErrorContext.from_code( 

282 endpoint=endpoint, 

283 error_code=AppPaymentErrorCode.CHARGE_AUTHORIZE_FAILED, 

284 cause=msg, 

285 error=e, 

286 ) 

287 return error.to_dict() 

288 

289 # ======================================================================== 

290 # CRON METHODS 

291 # ======================================================================== 

292 

293 def process_unprocessed_payment( # noqa: PLR0912, PLR0915 

294 self, 

295 pledge: PledgeWrapper, 

296 ) -> JobResult[PledgeWrapper]: 

297 # this is for payments in an error state, we try to re-process 

298 # and if that fails we fail 

299 doc_id = pledge.doc_id 

300 main_result = AppResult[PledgeWrapper]( 

301 doc_id=doc_id, task_name=f"Pledge unprocessed for {doc_id}" 

302 ) 

303 

304 if pledge.payment is None: 

305 msg = f"Pledge {pledge.doc_id} has no payment to process." 

306 LOG().error(msg) 

307 return self._build_job_result_error( 

308 endpoint="(internal)process_unprocessed_payment", 

309 main_result=main_result, 

310 error_code=AppPaymentErrorCode.PAYMENT_MISSING, 

311 message=msg, 

312 pledge=pledge, 

313 ) 

314 

315 payment_intent_id = pledge.payment.payment_intent_id 

316 endpoint = f"(internal) reauthorize_payment_intent {doc_id}/{payment_intent_id}" 

317 

318 if pledge.error_count >= PAYMENT_MAX_RETRIES: 

319 msg = ( 

320 f"Pledge {doc_id} exceeded max retries ({pledge.error_count}), marking as failed." 

321 ) 

322 pledge.payment_status = PaymentStatus.FAILED 

323 return self._build_job_result_error( 

324 endpoint=endpoint, 

325 error_code=AppPaymentErrorCode.PAYMENT_MAX_RETRIES_EXCEEDED, 

326 message=msg, 

327 pledge=pledge, 

328 main_result=main_result, 

329 ) 

330 

331 intent: PaymentIntentDTO | None = None 

332 try: 

333 intent = self.proxy.get_payment_intent( 

334 endpoint=endpoint, 

335 payment_intent_id=payment_intent_id, 

336 ) 

337 except Exception as err: 

338 msg = f"Failed to retrieve payment intent for reauthorization for {payment_intent_id}: {err}" 

339 return self._build_job_result_error( 

340 endpoint=endpoint, 

341 message=msg, 

342 pledge=pledge, 

343 main_result=main_result, 

344 ) 

345 

346 debug_msg = f"(pledge={doc_id}/payment_intent_id={payment_intent_id})" 

347 

348 validator_obj = self._create_validator(endpoint, pledge, intent, main_result) 

349 if isinstance(validator_obj, JobResult): 

350 return validator_obj 

351 

352 validator = validator_obj 

353 # first we check if we need reauthorize 

354 result: JobResult[PledgeWrapper] 

355 if validator.can_reauthorize: 

356 result = self.reauthorize_payment(pledge) 

357 if result.is_error: 

358 msg = f"Failed to reauthorize payment for {debug_msg} with error: {result.message}" 

359 LOG().error(msg) 

360 return result 

361 

362 # then check each state and try to process accordingly. 

363 if validator.needs_additional_info: 

364 # not much we can do here, we wait for the user to update their payment 

365 # method or whatever is required. 

366 additional_ct = pledge.additional_info_count 

367 if additional_ct >= PAYMENT_MAX_INFO_RETRIES: 

368 msg = f"Pledge {doc_id} exceeded max additional info retries ({additional_ct}), marking as failed." 

369 pledge.payment_status = PaymentStatus.FAILED 

370 return self._build_job_result_error( 

371 endpoint=endpoint, 

372 error_code=AppPaymentErrorCode.PAYMENT_MAX_INFO_RETRIES_EXCEEDED, 

373 message=msg, 

374 pledge=pledge, 

375 main_result=main_result, 

376 ) 

377 else: 

378 msg = f"Payment for {debug_msg} requires additional info, cannot process." 

379 pledge.additional_info_count += 1 

380 self._update_pledge(endpoint, pledge) 

381 LOG().info(msg) 

382 return JobResult.skip_doc(doc_id=doc_id, message=msg) 

383 

384 if validator.needs_capture: 

385 result = self.capture_payment(pledge) 

386 if result.is_error: 

387 msg = f"Failed to capture payment for {debug_msg} with error: {result.message}" 

388 LOG().error(msg) 

389 return result 

390 elif validator.needs_refund: 

391 result = self.refund_payment(pledge) 

392 if result.is_error: 

393 msg = f"Failed to refund payment for {debug_msg} with error: {result.message}" 

394 LOG().error(msg) 

395 return result 

396 elif validator.needs_transfer: 

397 result = self.transfer_payment(pledge) 

398 if result.is_error: 

399 msg = f"Failed to transfer payment for {debug_msg} with error: {result.message}" 

400 LOG().error(msg) 

401 return result 

402 

403 # This is potentially a problem ... 

404 # NOTE: we need to monitor the logs for these type of errors. 

405 # NOTE: they will probably require support intervention to resolve. 

406 msg = f"Payment for {debug_msg} is in an unknown state, cannot process." 

407 LOG().error(msg) 

408 return self._build_job_result_error( 

409 endpoint=endpoint, 

410 error_code=AppPaymentErrorCode.UNKNOWN_CHARGE_STATE, 

411 message=msg, 

412 pledge=pledge, 

413 main_result=main_result, 

414 ) 

415 

416 def reauthorize_payment(self, pledge: PledgeWrapper) -> JobResult[PledgeWrapper]: 

417 doc_id = pledge.doc_id 

418 main_result = AppResult[PledgeWrapper]( 

419 doc_id=doc_id, task_name=f"Pledge reauthorize for {doc_id}" 

420 ) 

421 

422 if pledge.payment is None: 

423 msg = f"Pledge {pledge.doc_id} has no payment to process." 

424 LOG().error(msg) 

425 return self._build_job_result_error( 

426 endpoint="(internal)process_unprocessed_payment", 

427 main_result=main_result, 

428 error_code=AppPaymentErrorCode.PAYMENT_MISSING, 

429 message=msg, 

430 pledge=pledge, 

431 ) 

432 

433 payment_intent_id = pledge.payment.payment_intent_id 

434 endpoint = f"(internal) reauthorize_payment_intent {doc_id}/{payment_intent_id}" 

435 

436 intent: PaymentIntentDTO | None = None 

437 try: 

438 intent = self.proxy.get_payment_intent( 

439 endpoint=endpoint, 

440 payment_intent_id=payment_intent_id, 

441 ) 

442 except Exception as err: 

443 msg = f"Failed to retrieve payment intent for reauthorization for {payment_intent_id}: {err}" 

444 return self._build_job_result_error( 

445 endpoint=endpoint, 

446 message=msg, 

447 pledge=pledge, 

448 main_result=main_result, 

449 ) 

450 

451 validator_obj = self._create_validator(endpoint, pledge, intent, main_result) 

452 if isinstance(validator_obj, JobResult): 

453 return validator_obj 

454 

455 validator = validator_obj 

456 if not validator.can_reauthorize: 

457 msg = f"Payment intent for pledge {doc_id} does not need reauthorization, skipping." 

458 LOG().info(msg) 

459 return JobResult.skip_doc(doc_id=doc_id, message=msg) 

460 

461 if not intent.nearing_timeout: 

462 msg = ( 

463 f"Payment intent {payment_intent_id} is not nearing timeout, cannot reauthorize yet." 

464 f"(intent_status={intent.intent_status}, event_status={intent.event_status})" 

465 ) 

466 LOG().warning(msg) 

467 return JobResult.skip_doc(doc_id=doc_id, message=msg) 

468 

469 # we need to cancel the existing intent 

470 # and create a new one with the same details to reauthorize the charge. 

471 try: 

472 self.proxy.cancel_payment_intent( 

473 endpoint=endpoint, 

474 payment_intent_id=payment_intent_id, 

475 ) 

476 except Exception as err: 

477 msg = f"Failed to cancel payment intent for reauthorization for {payment_intent_id}: {err}" 

478 LOG().error(msg) 

479 

480 main_result.add_error( 

481 error_code=AppPaymentErrorCode.API_ERROR, 

482 message=msg, 

483 ) 

484 return JobResult.from_result( 

485 main_result, 

486 data=pledge.to_json_dict(), 

487 message=msg, 

488 ) 

489 

490 # if we get here we have a valid payment. 

491 payment = pledge.payment 

492 payment_method_id = payment.payment_method_id 

493 assert payment_method_id is not None # narrowing 

494 

495 amount = payment.amount 

496 customer_id = payment.customer_id 

497 rcpt_currency_code = payment.customer_currency_code 

498 account_id = payment.account_id 

499 

500 try: 

501 self.proxy.reauthorize_payment_intent( 

502 endpoint=endpoint, 

503 customer_id=customer_id, 

504 account_id=account_id, 

505 amount=amount, 

506 fee_type=payment.fee_type, 

507 payment_method_id=payment_method_id, 

508 rcpt_currency_code=rcpt_currency_code, 

509 ) 

510 return JobResult.ok( 

511 doc_id=doc_id, 

512 message=f"Successfully reauthorized payment intent for charge {doc_id}", 

513 ) 

514 except Exception as err: 

515 msg = f"Failed to create new payment intent for reauthorization for {payment_intent_id}: {err}" 

516 LOG().error(msg) 

517 main_result.add_error( 

518 error_code=AppPaymentErrorCode.INTENT_INVALID_DATA, 

519 message=msg, 

520 ) 

521 return JobResult.from_result( 

522 main_result, 

523 data=pledge.to_json_dict(), 

524 message=msg, 

525 ) 

526 

527 def capture_payment( 

528 self, 

529 pledge: PledgeWrapper, 

530 ) -> JobResult[PledgeWrapper]: 

531 doc_id = pledge.doc_id 

532 payment = pledge.payment 

533 main_result = AppResult[PledgeWrapper]( 

534 doc_id=doc_id, task_name=f"Pledge capture for {doc_id}" 

535 ) 

536 

537 if payment is None: 

538 msg = f"Pledge {doc_id} has no payment to process." 

539 LOG().error(msg) 

540 return self._build_job_result_error( 

541 endpoint="(internal)process_unprocessed_payment", 

542 main_result=main_result, 

543 error_code=AppPaymentErrorCode.PAYMENT_MISSING, 

544 message=msg, 

545 pledge=pledge, 

546 ) 

547 

548 payment_intent_id = payment.payment_intent_id 

549 endpoint = f"(internal) capture_payment {doc_id}/{payment_intent_id}" 

550 

551 intent: PaymentIntentDTO | None = None 

552 try: 

553 intent = self.proxy.get_payment_intent( 

554 endpoint=endpoint, 

555 payment_intent_id=payment_intent_id, 

556 ) 

557 except Exception as err: 

558 msg = f"Failed to retrieve payment intent for capture for {payment_intent_id}: {err}" 

559 return self._build_job_result_error( 

560 endpoint=endpoint, 

561 message=msg, 

562 pledge=pledge, 

563 main_result=main_result, 

564 ) 

565 

566 validator_obj = self._create_validator(endpoint, pledge, intent, main_result) 

567 if isinstance(validator_obj, JobResult): 

568 return validator_obj 

569 

570 validator = validator_obj 

571 if not validator.needs_capture: 

572 msg = f"Payment intent for pledge {doc_id} cannot be captured, skipping." 

573 LOG().info(msg) 

574 return JobResult.skip_doc(doc_id=doc_id, message=msg) 

575 

576 payment_intent_id = payment.payment_intent_id 

577 account_id = payment.account_id 

578 

579 debug_msg = f"(acct_id={account_id}/pm_id={payment_intent_id})" 

580 

581 try: 

582 intent = self.proxy.CAPTURE_PAYMENT( 

583 endpoint=endpoint, 

584 payment_intent_id=payment_intent_id, 

585 ) 

586 except Exception as e: 

587 msg = f"Failed to charge for pledge {pledge.doc_id}: {debug_msg}\n\tError:{e}" 

588 LOG().error(msg) 

589 error = StripeErrorContext.from_code( 

590 endpoint=endpoint, 

591 error_code=AppPaymentErrorCode.PAYMENT_CAPTURE_FAILED, 

592 cause=msg, 

593 error=e, 

594 ) 

595 raise AppError.from_context(error) from e 

596 

597 intent_status = intent.intent_status 

598 payment_status = PaymentStatus.CAPTURE 

599 if intent_status == StripeIntentStatus.CANCELED: 

600 # mark as complete, we cant process any further.. 

601 payment_status = PaymentStatus.COMPLETE 

602 msg = f"Intent {payment_intent_id} was canceled, marking payment as COMPLETE. {debug_msg}" 

603 

604 if IS_DEBUG: 

605 LOG().debug(msg) 

606 

607 pledge.payment_status = payment_status 

608 self._update_pledge(endpoint, pledge) 

609 return JobResult.ok(doc_id=doc_id, msg=msg) 

610 

611 # process event 

612 event_status = intent.event_status 

613 charge_id = intent.latest_charge_id 

614 result = PaymentResultModel( 

615 amount_captured=intent.amount_captured or 0, 

616 stripe_fee_amount=intent.stripe_fee_amount or 0, 

617 app_fee_amount=intent.app_fee_amount or 0, 

618 ) 

619 

620 if intent_status == StripeIntentStatus.SUCCEEDED and charge_id is not None: 

621 msg = f"Intent {payment_intent_id} captured successfully. {debug_msg}" 

622 if IS_DEBUG: 

623 LOG().debug(msg) 

624 

625 return self._add_payment_success_event( 

626 endpoint=endpoint, 

627 message=msg, 

628 pledge=pledge, 

629 intent_status=intent_status, 

630 stripe_id=charge_id, 

631 result=result, 

632 payment_status=payment_status, 

633 success_type="captured", 

634 ) 

635 

636 # if we get here, the charge was not successful, 

637 # we need to log an error and add a payment event for the failed charge. 

638 validation_msg = intent.validate_capture() 

639 msg = ( 

640 f"Payment failed validation or incorrect status for {debug_msg}" 

641 f"\n\tValidation message = {validation_msg}" 

642 f"\n\tCharge ID: = {charge_id}" 

643 f"\n\tIntent status = {intent_status}" 

644 f"\n\tEvent status: = {event_status}" 

645 ) 

646 LOG().error(msg) 

647 

648 return self._add_payment_error_event( 

649 endpoint=endpoint, 

650 message=msg, 

651 pledge=pledge, 

652 payment_result=result, 

653 intent_status=intent_status, 

654 event_status=event_status, 

655 app_result=main_result, 

656 payment_status=payment_status, 

657 ) 

658 

659 def refund_payment( 

660 self, 

661 pledge: PledgeWrapper, 

662 ) -> JobResult[PledgeWrapper]: 

663 doc_id = pledge.doc_id 

664 payment = pledge.payment 

665 main_result = AppResult[PledgeWrapper]( 

666 doc_id=doc_id, task_name=f"Pledge refund for {doc_id}" 

667 ) 

668 

669 if payment is None: 

670 msg = f"Pledge {doc_id} has no payment to process." 

671 LOG().error(msg) 

672 return self._build_job_result_error( 

673 endpoint="(internal)refund_payment", 

674 main_result=main_result, 

675 error_code=AppPaymentErrorCode.PAYMENT_MISSING, 

676 message=msg, 

677 pledge=pledge, 

678 ) 

679 

680 payment_intent_id = payment.payment_intent_id 

681 

682 endpoint = f"(internal) refund_payment {doc_id}/{payment_intent_id}" 

683 main_result = AppResult[PledgeWrapper](doc_id=doc_id, task_name=f"{endpoint} for {doc_id}") 

684 

685 intent: PaymentIntentDTO | None = None 

686 try: 

687 intent = self.proxy.get_payment_intent( 

688 endpoint=endpoint, 

689 payment_intent_id=payment_intent_id, 

690 ) 

691 except Exception as err: 

692 msg = f"Failed to retrieve payment intent for refund for {payment_intent_id}: {err}" 

693 return self._build_job_result_error( 

694 endpoint=endpoint, 

695 message=msg, 

696 pledge=pledge, 

697 main_result=main_result, 

698 ) 

699 

700 validator_obj = self._create_validator(endpoint, pledge, intent, main_result) 

701 if isinstance(validator_obj, JobResult): 

702 return validator_obj 

703 

704 validator = validator_obj 

705 if not validator.needs_refund: 

706 msg = f"Payment intent for pledge {doc_id} cannot be refunded, skipping." 

707 LOG().info(msg) 

708 return JobResult.skip_doc(doc_id=doc_id, message=msg) 

709 

710 events: list[PaymentEventWrapper] = [] 

711 try: 

712 events = self.pledge_db.get_payment_events(pledge.doc_id) 

713 except Exception as e: 

714 msg = f"Failed to get payment events for release for pledge {doc_id}: {e}" 

715 LOG().error(msg) 

716 main_result.add_error( 

717 error_code=AppPaymentErrorCode.API_ERROR, 

718 message=msg, 

719 ) 

720 return JobResult.from_result( 

721 main_result, 

722 data=pledge.to_json_dict(), 

723 message=msg, 

724 ) 

725 

726 evt_ctx = PaymentEventContext(amount=payment.amount, events=events) 

727 refund_amount = evt_ctx.refundable_amount 

728 if refund_amount <= 0: 

729 msg = f"Payment intent for pledge {doc_id} cannot be refunded because invalid amount ({refund_amount}), skipping." 

730 LOG().info(msg) 

731 return JobResult.skip_doc(doc_id=doc_id, message=msg) 

732 

733 # if we get here the payment needs to be refunded and the latest_charge_id is not None. 

734 account_id = payment.account_id 

735 

736 debug_msg = f"(acct_id={account_id}/pm_id={payment_intent_id})" 

737 

738 try: 

739 refund = self.proxy.REFUND_PAYMENT( 

740 endpoint=endpoint, 

741 customer_id=payment.customer_id, 

742 obj_id=pledge.dare_id, 

743 payment_intent_id=payment_intent_id, 

744 refund_amount=refund_amount, 

745 ) 

746 except Exception as e: 

747 msg = f"Failed to release payment for pledge {pledge.doc_id}: {debug_msg}\n\tError:{e}" 

748 LOG().error(msg) 

749 error = StripeErrorContext.from_code( 

750 endpoint=endpoint, 

751 error_code=AppPaymentErrorCode.TRANSFER_FAILED, 

752 cause=msg, 

753 error=e, 

754 ) 

755 raise AppError.from_context(error) from e 

756 

757 # if we get here the transfer was successful... 

758 result = PaymentResultModel( 

759 amount_refunded=refund.amount, 

760 ) 

761 

762 msg = f"Payment for pledge {doc_id} refunded successfully with refund {refund.id}/{refund.amount}" 

763 if IS_DEBUG: 

764 LOG().debug(msg) 

765 

766 return self._add_payment_success_event( 

767 endpoint=endpoint, 

768 message=msg, 

769 pledge=pledge, 

770 intent_status=StripeIntentStatus.SUCCEEDED, 

771 stripe_id=refund.id, 

772 result=result, 

773 payment_status=PaymentStatus.COMPLETE, 

774 success_type="refunded", 

775 ) 

776 

777 def transfer_payment( 

778 self, 

779 pledge: PledgeWrapper, 

780 ) -> JobResult[PledgeWrapper]: 

781 payment = pledge.payment 

782 if payment is None: 

783 msg = f"Pledge {pledge.doc_id} has no payment information, cannot transfer." 

784 LOG().error(msg) 

785 return self._build_job_result_error( 

786 endpoint="(internal) transfer_payment", 

787 message=msg, 

788 pledge=pledge, 

789 main_result=AppResult[PledgeWrapper]( 

790 doc_id=pledge.doc_id, task_name="transfer_payment" 

791 ), 

792 ) 

793 

794 doc_id = pledge.doc_id 

795 payment_intent_id = payment.payment_intent_id 

796 

797 endpoint = f"(internal) transfer_payment {doc_id}/{payment_intent_id}" 

798 main_result = AppResult[PledgeWrapper](doc_id=doc_id, task_name=f"{endpoint} for {doc_id}") 

799 

800 intent: PaymentIntentDTO | None = None 

801 try: 

802 intent = self.proxy.get_payment_intent( 

803 endpoint=endpoint, 

804 payment_intent_id=payment_intent_id, 

805 ) 

806 except Exception as err: 

807 msg = f"Failed to retrieve payment intent for transfer for {payment_intent_id}: {err}" 

808 return self._build_job_result_error( 

809 endpoint=endpoint, 

810 message=msg, 

811 pledge=pledge, 

812 main_result=main_result, 

813 ) 

814 

815 validator_obj = self._create_validator(endpoint, pledge, intent, main_result) 

816 if isinstance(validator_obj, JobResult): 

817 return validator_obj 

818 

819 validator = validator_obj 

820 if not validator.needs_transfer: 

821 msg = f"Payment intent for pledge {doc_id} cannot be transferred, skipping." 

822 LOG().info(msg) 

823 return JobResult.skip_doc(doc_id=doc_id, message=msg) 

824 

825 events: list[PaymentEventWrapper] = [] 

826 try: 

827 events = self.pledge_db.get_payment_events(pledge.doc_id) 

828 except Exception as e: 

829 msg = f"Failed to get payment events for release for pledge {doc_id}: {e}" 

830 LOG().error(msg) 

831 main_result.add_error( 

832 error_code=AppPaymentErrorCode.API_ERROR, 

833 message=msg, 

834 ) 

835 return JobResult.from_result( 

836 main_result, 

837 data=pledge.to_json_dict(), 

838 message=msg, 

839 ) 

840 

841 evt_ctx = PaymentEventContext(amount=payment.amount, events=events) 

842 transfer_amount = evt_ctx.transferrable_amount 

843 if transfer_amount <= 0: 

844 msg = f"Pledge ({doc_id}) has invalid transfer amount ({transfer_amount}), skipping." 

845 LOG().info(msg) 

846 return JobResult.skip_doc(doc_id=doc_id, message=msg) 

847 

848 # if we get here the payment needs to be transfer and the latest_charge_id is not None. 

849 account_id = payment.account_id 

850 transfer_currency = payment.account_currency_code 

851 charge_id = payment.latest_charge_id 

852 assert charge_id is not None # narrowing 

853 

854 debug_msg = f"(acct_id={account_id}/pm_id={payment_intent_id})" 

855 

856 try: 

857 transfer = self.proxy.TRANSFER_PAYMENT( 

858 endpoint=endpoint, 

859 customer_id=payment.customer_id, 

860 obj_id=pledge.dare_id, 

861 account_id=account_id, 

862 charge_id=charge_id, 

863 transfer_amount=transfer_amount, 

864 transfer_currency=transfer_currency, 

865 ) 

866 except Exception as e: 

867 msg = f"Failed to release payment for pledge {pledge.doc_id}: {debug_msg}\n\tError:{e}" 

868 LOG().error(msg) 

869 error = StripeErrorContext.from_code( 

870 endpoint=endpoint, 

871 error_code=AppPaymentErrorCode.TRANSFER_FAILED, 

872 cause=msg, 

873 error=e, 

874 ) 

875 raise AppError.from_context(error) from e 

876 

877 # if we get here the transfer was successful... 

878 result = PaymentResultModel( 

879 amount_transferred=transfer.amount, 

880 ) 

881 

882 msg = f"Payment for pledge {doc_id} transferred successfully with transfer {transfer.id}/{transfer.amount}" 

883 if IS_DEBUG: 

884 LOG().debug(msg) 

885 

886 return self._add_payment_success_event( 

887 endpoint=endpoint, 

888 message=msg, 

889 pledge=pledge, 

890 intent_status=StripeIntentStatus.SUCCEEDED, 

891 stripe_id=transfer.id, 

892 result=result, 

893 payment_status=PaymentStatus.COMPLETE, 

894 success_type="transferred", 

895 ) 

896 

897 # ======================================================================== 

898 # HELPERS 

899 # ======================================================================== 

900 

901 def _create_validator( 

902 self, 

903 endpoint: str, 

904 pledge: PledgeWrapper, 

905 intent: PaymentIntentDTO, 

906 main_result: AppResult[PledgeWrapper], 

907 ) -> PaymentValidator | JobResult[PledgeWrapper]: 

908 doc_id = pledge.doc_id 

909 payment = pledge.payment 

910 

911 if payment is None: 

912 msg = f"Pledge {doc_id} has no payment information, cannot validate." 

913 LOG().error(msg) 

914 return self._build_job_result_error( 

915 endpoint=endpoint, 

916 message=msg, 

917 pledge=pledge, 

918 main_result=main_result, 

919 ) 

920 

921 validator = PaymentValidator(pledge) 

922 if validator.needs_refresh: 

923 payment.intent_status = intent.intent_status 

924 

925 validator.set_refreshed_payment(payment) 

926 if validator.needs_refresh: 

927 msg = f"Payment intent for pledge {doc_id} failed to refresh, cannot reauthorize." 

928 return self._build_job_result_error( 

929 endpoint=endpoint, 

930 message=msg, 

931 pledge=pledge, 

932 main_result=main_result, 

933 ) 

934 

935 if validator.has_errors: 

936 msg = f"Payment intent for pledge {doc_id} has validation errors after refresh, cannot reauthorize." 

937 return self._build_job_result_error( 

938 endpoint=endpoint, 

939 message=msg, 

940 pledge=pledge, 

941 main_result=main_result, 

942 ) 

943 

944 return validator 

945 

946 def _build_job_result_error( 

947 self, 

948 endpoint: str, 

949 message: str, 

950 pledge: PledgeWrapper, 

951 main_result: AppResult[PledgeWrapper], 

952 error_code: AppPaymentErrorCode = AppPaymentErrorCode.API_ERROR, 

953 ) -> JobResult[PledgeWrapper]: 

954 LOG().error(f"Charge Error for pledge {pledge.doc_id} at endpoint {endpoint}: {message}") 

955 

956 pledge.error_count += 1 

957 try: 

958 pledge.error_count += 1 

959 self._update_pledge(endpoint, pledge) 

960 except Exception as e: 

961 msg = f"Failed to update pledge {pledge.doc_id} after error: {e}" 

962 LOG().error(msg) 

963 main_result.add_error(AppPaymentErrorCode.DATABASE_ERROR, msg, extra=str(e)) 

964 

965 main_result.add_error( 

966 error_code=error_code, 

967 message=message, 

968 ) 

969 return JobResult.from_result( 

970 main_result, 

971 data=pledge.to_json_dict(), 

972 message=message, 

973 ) 

974 

975 def _refresh_schedule(self, endpoint: str, pledge: PledgeWrapper) -> None: 

976 events: list[PaymentEventWrapper] = [] 

977 

978 payment = pledge.payment 

979 if payment is None: 

980 msg = f"Pledge {pledge.doc_id} has no payment information, cannot validate." 

981 LOG().error(msg) 

982 raise AppError.from_context( 

983 StripeErrorContext.from_code( 

984 endpoint=endpoint, 

985 error_code=AppPaymentErrorCode.PAYMENT_MISSING, 

986 cause=msg, 

987 ) 

988 ) 

989 

990 try: 

991 events = self.pledge_db.get_payment_events(pledge.doc_id) 

992 except Exception as e: 

993 msg = f"Refresh failed, failed to get events for {pledge.doc_id}\n\tError:{e}" 

994 LOG().error(msg) 

995 raise AppError.from_context( 

996 StripeErrorContext.from_code( 

997 endpoint=endpoint, 

998 error_code=AppPaymentErrorCode.DATABASE_ERROR, 

999 cause=msg, 

1000 error=e, 

1001 ) 

1002 ) from e 

1003 

1004 schedule = PaymentSchedule(pledge.doc_id, payment, events) 

1005 if not schedule.can_schedule: 

1006 msg = f"Incomplete pledge info, cannot refresh payment schedule for pledge {pledge.doc_id}." 

1007 LOG().error(msg) 

1008 raise AppError.from_context( 

1009 StripeErrorContext.from_code( 

1010 endpoint=endpoint, 

1011 error_code=AppPaymentErrorCode.SCHEDULE_ERROR, 

1012 cause=msg, 

1013 error=None, 

1014 ) 

1015 ) 

1016 

1017 updated_schedule = schedule.updated_schedule 

1018 updated_status = schedule.updated_status 

1019 if updated_status is None and updated_schedule is None: 

1020 if IS_DEBUG: 

1021 msg = f"Schedule updates not required for pledge {pledge.doc_id}, skipping update." 

1022 LOG().debug(msg) 

1023 return 

1024 

1025 if updated_schedule is not None: 

1026 pledge.payment_schedule = updated_schedule 

1027 

1028 if updated_status is not None: 

1029 pledge.payment_status = updated_status 

1030 

1031 self._update_pledge(endpoint, pledge) 

1032 

1033 def _refresh_intent_status( 

1034 self, 

1035 endpoint: str, 

1036 payment_intent_id: str, 

1037 ) -> tuple[StripeIntentStatus, PaymentEventStatus] | None: 

1038 try: 

1039 intent = self.proxy.get_payment_intent( 

1040 endpoint=endpoint, 

1041 payment_intent_id=payment_intent_id, 

1042 ) 

1043 return intent.intent_status, intent.event_status 

1044 

1045 except Exception as e: 

1046 msg = f"Failed to refresh payment intent status for {payment_intent_id}: {e}" 

1047 LOG().error(msg) 

1048 return None 

1049 

1050 def _build_payment_context( 

1051 self, 

1052 audit_info: AuditInfoModel, 

1053 risk_info: AccountRiskInfo, 

1054 cust_user: UserWrapper, 

1055 dare_id: str, 

1056 amount: int, 

1057 ) -> AppPaymentContext: 

1058 return AppPaymentContext.create_from_user( 

1059 audit_info=audit_info, 

1060 risk_assessment=risk_info.risk_assessment, 

1061 dare_id=dare_id, 

1062 customer=cust_user, 

1063 account=risk_info.user, 

1064 amount=amount, 

1065 ) 

1066 

1067 def get_account_risk( 

1068 self, 

1069 endpoint: str, 

1070 account_uid: str, 

1071 ) -> AccountRiskInfo: 

1072 acct_user: UserWrapper | None = None 

1073 try: 

1074 acct_user = self.get_user_by_id(endpoint=endpoint, uid=account_uid) 

1075 except Exception as e: 

1076 msg = f"Failed build payment context for {account_uid}\n\tError:{e}" 

1077 LOG().error(msg) 

1078 raise AppError.from_context( 

1079 StripeErrorContext.from_code( 

1080 endpoint=endpoint, 

1081 error_code=AppPaymentErrorCode.INVALID_USER, 

1082 cause=msg, 

1083 error=e, 

1084 ) 

1085 ) from e 

1086 

1087 settings = acct_user.stripe_settings 

1088 if not StripeGuard.is_account(settings): 

1089 msg = ( 

1090 f"User {account_uid} has invalid stripe settings, cannot build account risk info." 

1091 ) 

1092 LOG().error(msg) 

1093 raise AppError.from_context( 

1094 StripeErrorContext.from_code( 

1095 endpoint=endpoint, 

1096 error_code=AppPaymentErrorCode.INVALID_USER, 

1097 cause=msg, 

1098 error=None, 

1099 ) 

1100 ) 

1101 

1102 risk_assessment = self.risk_service.calculate_risk( 

1103 settings=settings, 

1104 fee_type=acct_user.fee_type, 

1105 ) 

1106 return AccountRiskInfo( 

1107 user=acct_user, 

1108 risk_assessment=risk_assessment, 

1109 ) 

1110 

1111 def _add_payment_success_event( 

1112 self, 

1113 endpoint: str, 

1114 message: str, 

1115 pledge: PledgeWrapper, 

1116 intent_status: StripeIntentStatus, 

1117 stripe_id: str, 

1118 result: PaymentResultModel, 

1119 payment_status: PaymentStatus, 

1120 success_type: Literal["captured", "transferred", "refunded"], 

1121 ) -> JobResult[PledgeWrapper]: 

1122 doc_id = pledge.doc_id 

1123 

1124 payment = pledge.payment 

1125 if payment is None: 

1126 msg = f"Pledge {doc_id} has no payment information, cannot add payment success event." 

1127 LOG().error(msg) 

1128 error = StripeErrorContext.from_code( 

1129 endpoint=endpoint, 

1130 error_code=AppPaymentErrorCode.PLEDGE_MISSING_PAYMENT, 

1131 cause=msg, 

1132 error=None, 

1133 ) 

1134 raise AppError.from_context(error) 

1135 

1136 evt = PaymentEventModel.success( 

1137 audit_info=payment.audit_info, 

1138 intent_status=intent_status, 

1139 stripe_id=stripe_id, 

1140 result=result, 

1141 success_type=success_type, 

1142 ) 

1143 

1144 try: 

1145 self._add_payment_event_to_db( 

1146 endpoint=endpoint, 

1147 pledge=pledge, 

1148 evt=evt, 

1149 payment_status=payment_status, 

1150 result=result, 

1151 ) 

1152 self._refresh_schedule(endpoint, pledge) 

1153 except Exception as e: 

1154 msg = f"Failed to add payment event to db for captured charge for pledge {pledge.doc_id}\n\tError:{e}" 

1155 LOG().error(msg) 

1156 error = StripeErrorContext.from_code( 

1157 endpoint=endpoint, 

1158 error_code=AppPaymentErrorCode.DATABASE_ERROR, 

1159 cause=msg, 

1160 error=e, 

1161 ) 

1162 raise AppError.from_context(error) from e 

1163 

1164 return JobResult.ok(doc_id=doc_id, message=message) 

1165 

1166 def _add_payment_error_event( 

1167 self, 

1168 endpoint: str, 

1169 message: str, 

1170 pledge: PledgeWrapper, 

1171 intent_status: StripeIntentStatus, 

1172 payment_status: PaymentStatus, 

1173 event_status: PaymentEventStatus, 

1174 payment_result: PaymentResultModel, 

1175 app_result: AppResult[PledgeWrapper], 

1176 ) -> JobResult[PledgeWrapper]: 

1177 payment = pledge.payment 

1178 if payment is None: 

1179 msg = f"Pledge {pledge.doc_id} has no payment information, cannot add payment error event." 

1180 LOG().error(msg) 

1181 error = StripeErrorContext.from_code( 

1182 endpoint=endpoint, 

1183 error_code=AppPaymentErrorCode.PLEDGE_MISSING_PAYMENT, 

1184 cause=msg, 

1185 error=None, 

1186 ) 

1187 raise AppError.from_context(error) 

1188 

1189 evt = PaymentEventModel.error( 

1190 audit_info=payment.audit_info, 

1191 result=payment_result, 

1192 error_message=message, 

1193 intent_status=intent_status, 

1194 status=event_status, 

1195 ) 

1196 try: 

1197 self._add_payment_event_to_db( 

1198 endpoint=endpoint, 

1199 pledge=pledge, 

1200 evt=evt, 

1201 payment_status=payment_status, 

1202 result=payment_result, 

1203 ) 

1204 except Exception as e: 

1205 # we cant do much here, we hope the log parser will pick up these errors.. 

1206 msg = f"Failed to add payment event to db for failed charge for pledge {pledge.doc_id}: {message}\n\tError:{e}" 

1207 LOG().error(msg) 

1208 

1209 app_result.add_error( 

1210 error_code=AppPaymentErrorCode.PAYMENT_CAPTURE_FAILED, 

1211 message=message, 

1212 ) 

1213 return JobResult.from_result( 

1214 app_result, 

1215 data=pledge.to_json_dict(), 

1216 message=message, 

1217 ) 

1218 

1219 # ======================================================================== 

1220 # DATABASE 

1221 # ======================================================================== 

1222 

1223 def _get_pledge(self, endpoint: str, pledge_id: str) -> PledgeWrapper: 

1224 # dont use pledge is not None, lint checkers dont recognize throw_ wont return 

1225 pledge: PledgeWrapper | None = None 

1226 try: 

1227 pledge = self.pledge_db.get(pledge_id) 

1228 except Exception as e: 

1229 msg = f"Failed to retrieve pledge for id {pledge_id} when updating payment method\n\tError:{e}" 

1230 LOG().error(msg) 

1231 raise AppError.from_context( 

1232 StripeErrorContext.from_code( 

1233 endpoint=endpoint, 

1234 error_code=AppPaymentErrorCode.DATABASE_ERROR, 

1235 cause=msg, 

1236 error=e, 

1237 ) 

1238 ) from e 

1239 

1240 if pledge is not None: 

1241 return pledge 

1242 

1243 msg = f"No pledge found for id {pledge_id} when updating payment method." 

1244 LOG().error(msg) 

1245 raise AppError.from_context( 

1246 StripeErrorContext.from_code( 

1247 endpoint=endpoint, 

1248 error_code=AppPaymentErrorCode.PLEDGE_MISSING, 

1249 cause=msg, 

1250 ) 

1251 ) 

1252 

1253 def _get_pledge_for_payment_intent( 

1254 self, 

1255 endpoint: str, 

1256 payment_intent_id: str, 

1257 ) -> PledgeWrapper | None: 

1258 try: 

1259 return self.pledge_db.get_by_payment_intent_id(payment_intent_id) 

1260 except Exception as e: 

1261 msg = ( 

1262 f"Failed to retrieve pledge for payment intent id {payment_intent_id} " 

1263 f"when processing charge webhook\n\tError:{e}" 

1264 ) 

1265 LOG().error(msg) 

1266 raise AppError.from_context( 

1267 StripeErrorContext.from_code( 

1268 endpoint=endpoint, 

1269 error_code=AppPaymentErrorCode.INVALID_REQUEST, 

1270 cause=msg, 

1271 error=e, 

1272 ) 

1273 ) from e 

1274 

1275 def _create_pledge_in_db( 

1276 self, 

1277 ctx: AppPaymentContext, 

1278 payment_intent_id: str, 

1279 gid: str | None = None, 

1280 ) -> PledgeWrapper: 

1281 from_uid = ctx.customer.uid 

1282 to_uid = ctx.account.uid 

1283 dare_id = ctx.dare_id 

1284 amount = ctx.amount 

1285 currency_code = ctx.account.currency_code 

1286 

1287 pledge_model = PledgeModel( 

1288 id=None, 

1289 slug_code=SlugCoder().from_doc_id(dare_id), 

1290 from_uid=from_uid, 

1291 to_uid=to_uid, 

1292 dare_id=dare_id, 

1293 gid=gid, 

1294 amount=amount, 

1295 currency_code=currency_code, 

1296 payment=PaymentModel( 

1297 audit_info=ctx.audit_info, 

1298 risk_assessment=ctx.risk_assessment, 

1299 customer_info=ctx.customer, 

1300 account_info=ctx.account, 

1301 payment_intent_id=payment_intent_id, 

1302 amount=amount, 

1303 ), 

1304 ) 

1305 try: 

1306 return self.pledge_db.create(pledge_model) 

1307 except Exception as e: 

1308 msg = f"Failed to create pledge in db for dare {dare_id} from {from_uid} to {to_uid}\n\tError:{e}" 

1309 LOG().error(msg) 

1310 raise AppError.from_context( 

1311 StripeErrorContext.from_code( 

1312 endpoint="(internal)_create_pledge_in_db", 

1313 error_code=AppPaymentErrorCode.API_ERROR, 

1314 cause=msg, 

1315 error=e, 

1316 ) 

1317 ) from e 

1318 

1319 def _update_payment_method( 

1320 self, 

1321 endpoint: str, 

1322 pledge_id: str, 

1323 payment_method_id: str, 

1324 ) -> PledgeWrapper | None: 

1325 

1326 debug_msg = f"(pledge {pledge_id})" 

1327 pledge = self._get_pledge(endpoint, pledge_id) 

1328 

1329 try: 

1330 pledge.payment_method_id = payment_method_id 

1331 return self._update_pledge(endpoint, pledge) 

1332 except Exception as e: 

1333 # most likely no charge associated with the pledge 

1334 msg = f"Failed to set payment method id for {debug_msg} when updating payment method\n\tError:{e}" 

1335 LOG().error(msg) 

1336 raise AppError.from_context( 

1337 StripeErrorContext.from_code( 

1338 endpoint=endpoint, 

1339 error_code=AppPaymentErrorCode.INVALID_REQUEST, 

1340 cause=msg, 

1341 error=e, 

1342 ) 

1343 ) from e 

1344 

1345 def _update_pledge( 

1346 self, 

1347 endpoint: str, 

1348 pledge: PledgeWrapper, 

1349 ) -> PledgeWrapper | None: 

1350 

1351 debug_msg = f"(pledge {pledge.doc_id})" 

1352 try: 

1353 updates = pledge.get_updates() 

1354 if not updates: 

1355 msg = f"No changes to update for pledge {debug_msg} when updating payment method." 

1356 LOG().warning(msg) 

1357 return None 

1358 

1359 updated = self.pledge_db.update(pledge.doc_id, updates) 

1360 except Exception as e: 

1361 msg = f"Failed to update pledge {debug_msg} when updating payment method\n\tError:{e}" 

1362 LOG().error(msg) 

1363 raise AppError.from_context( 

1364 StripeErrorContext.from_code( 

1365 endpoint=endpoint, 

1366 error_code=AppPaymentErrorCode.API_ERROR, 

1367 cause=msg, 

1368 error=e, 

1369 ) 

1370 ) from e 

1371 

1372 if updated is not None: 

1373 return updated 

1374 

1375 msg = f"Failed to update pledge {debug_msg} with new charge info when updating payment method." 

1376 LOG().error(msg) 

1377 raise AppError.from_context( 

1378 StripeErrorContext.from_code( 

1379 endpoint=endpoint, 

1380 error_code=AppPaymentErrorCode.API_ERROR, 

1381 cause=msg, 

1382 ) 

1383 ) 

1384 

1385 def _add_payment_event_to_db( 

1386 self, 

1387 endpoint: str, 

1388 pledge: PledgeWrapper, 

1389 evt: PaymentEventModel, 

1390 payment_status: PaymentStatus, 

1391 result: PaymentResultModel, 

1392 ) -> PaymentEventWrapper | None: 

1393 # adds a charge event to the pledge db sub-collection. 

1394 try: 

1395 return self.pledge_db.add_payment_event(pledge, evt, payment_status, result) 

1396 except Exception as e: 

1397 msg = f"Failed to create charge event for pledge {pledge.doc_id} with event {evt}\n\tError:{e}" 

1398 LOG().error(msg) 

1399 raise AppError.from_context( 

1400 StripeErrorContext.from_code( 

1401 endpoint=endpoint, 

1402 error_code=AppPaymentErrorCode.API_ERROR, 

1403 cause=msg, 

1404 error=e, 

1405 ) 

1406 ) from e