Coverage for functions \ flipdare \ service \ payments \ _payment_account_handler.py: 46%

375 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 

14 

15from typing import TYPE_CHECKING, Any 

16from dataclasses import dataclass 

17from firebase_functions import https_fn 

18from flipdare.app_log import LOG 

19from flipdare.constants import IS_DEBUG, IS_TRACE 

20from flipdare.error import ( 

21 AppError, 

22 StripeErrorContext, 

23) 

24from flipdare.generated import ( 

25 StripeCreateAccountResponseSchema, 

26 AppPaymentErrorCode, 

27 StripeCurrencyCode, 

28 StripeOnboardState, 

29 ErrorSchema, 

30 StripeAccountType, 

31 StripeCountryCode, 

32 StripeRefreshAccountResponseSchema, 

33 StripeAccountModel, 

34 StripeCustomerModel, 

35 StripeUpgradeCustomerResponseSchema, 

36) 

37from flipdare.request.data.stripe_request_adapter import StripeUpgradeCustomerRequestAdapter 

38from flipdare.service._error_mixin import ErrorMixin 

39from flipdare.service._service_provider import ServiceProvider 

40from flipdare.service._user_mixin import UserMixin 

41from flipdare.service.payments._base_payment_handler import BasePaymentHandler 

42from flipdare.request import ( 

43 AppRequest, 

44 StripeCreateAccountRequestAdapter, 

45 StripeRefreshAccountRequestAdapter, 

46) 

47from flipdare.payments.app_stripe_proxy import AppStripeProxy 

48from flipdare.payments.dto.account_dto import AccountDTO 

49from flipdare.payments.core.stripe_guard import StripeGuard 

50from flipdare.payments.core.stripe_util import StripeUtil 

51from flipdare.wrapper.user_wrapper import UserWrapper 

52 

53if TYPE_CHECKING: 

54 from flipdare.manager.service_manager import ServiceManager 

55 from flipdare.manager.backend_manager import BackendManager 

56 from flipdare.manager.db_manager import DbManager 

57 from flipdare.service.payments._payment_link_handler import PaymentLinkHandler 

58 

59__all__ = ["PaymentAccountHandler"] 

60 

61 

62class PaymentAccountHandler(BasePaymentHandler, ErrorMixin, UserMixin, ServiceProvider): 

63 

64 def __init__( 

65 self, 

66 proxy: AppStripeProxy, 

67 link_handler: PaymentLinkHandler, 

68 db_manager: DbManager | None = None, 

69 backend_manager: BackendManager | None = None, 

70 service_manager: ServiceManager | None = None, 

71 ) -> None: 

72 self.proxy = proxy 

73 self.link_handler = link_handler 

74 

75 super().__init__( 

76 db_manager=db_manager, 

77 backend_manager=backend_manager, 

78 service_manager=service_manager, 

79 ) 

80 

81 # ---------------------------------------------------------------------------------------------- 

82 # CALLABLE REQUESTS 

83 # ---------------------------------------------------------------------------------------------- 

84 

85 def callable_create_account( # noqa: PLR0915 

86 self, 

87 req: https_fn.CallableRequest[Any], 

88 ) -> StripeCreateAccountResponseSchema | ErrorSchema: 

89 """ 

90 Create a new Stripe account or return existing account link. 

91 

92 Returns: 

93 StripeCreateAccountResponseSchema on success 

94 ErrorSchema on validation or creation failure 

95 

96 """ 

97 request = AppRequest.callable(req) 

98 endpoint = request.endpoint 

99 

100 # Guard: Validate request data 

101 account_data: StripeCreateAccountRequestAdapter | None = None 

102 try: 

103 account_data = StripeCreateAccountRequestAdapter.from_callable(req) 

104 account_data.validate() 

105 except AppError as e: 

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

107 error = StripeErrorContext.from_code( 

108 endpoint="(internal)create_account", 

109 error_code=AppPaymentErrorCode.INVALID_REQUEST, 

110 cause=msg, 

111 error=e, 

112 ) 

113 return error.to_dict() 

114 

115 # Extract validated parameters 

116 uid = account_data.uid 

117 account_type = account_data.account_type 

118 first_name = account_data.first_name 

119 last_name = account_data.last_name 

120 country_code = account_data.country_code 

121 currency_code = account_data.currency_code 

122 overwrite = account_data.overwrite 

123 

124 validated = self._validate_account( 

125 endpoint=endpoint, 

126 uid=uid, 

127 account_must_exist=False, 

128 ) 

129 

130 if validated.is_error: 

131 assert validated.error is not None # narrowing 

132 return validated.error.to_dict() 

133 

134 user = validated.user 

135 user_id = user.doc_id 

136 settings = validated.account_settings 

137 

138 email = account_data.email or user.email 

139 error_code: AppPaymentErrorCode = AppPaymentErrorCode.ACCOUNT_CREATE_FAILED 

140 

141 if settings is not None: 

142 account_id = settings.account_id 

143 # we have existing settings ... 

144 # so we can create a link, unless overwrite is True 

145 if not overwrite: 

146 link_response = self._create_onboard_link_response( 

147 endpoint=endpoint, 

148 user=user, 

149 account_id=account_id, 

150 overwrite=overwrite, 

151 error_code=error_code, 

152 ) 

153 

154 if link_response.is_error: 

155 # will only error if existing account is invalid, so we can continue to create a new account 

156 err = link_response.error 

157 msg = ( 

158 f"Exception refreshing link for existing account for User ({user_id}/{account_id}), " 

159 f"creating new account.\n\tError:{err}" 

160 ) 

161 LOG().error(msg) 

162 else: 

163 if IS_DEBUG: 

164 msg = f"User {user_id} already has an account, returning existing onboard link." 

165 LOG().debug(msg) 

166 

167 link = link_response.account_link 

168 return StripeCreateAccountResponseSchema( 

169 uid=user_id, 

170 url=link.url, 

171 expires_at=link.expires_at, 

172 account_id=link.account_id, 

173 ) 

174 

175 # Create customer (non-critical, continue on failure) 

176 if IS_DEBUG: 

177 msg = ( 

178 f"Creating Stripe account for user {user_id} with data:\n" 

179 f"\tAccount Type: {account_type}\n" 

180 f"\tFirst Name: {first_name}\n" 

181 f"\tLast Name: {last_name}\n" 

182 f"\tCountry Code: {country_code}\n" 

183 f"\tCurrency Code: {currency_code}\n" 

184 f"\tName Tokens: {(first_name, last_name)}\n" 

185 ) 

186 LOG().debug(msg) 

187 

188 # NOTE: we have migrated to Stripe V2 customer accounts 

189 # NOTE: so the acct_id is also the customer_id 

190 create_obj = self._create_account( 

191 endpoint=endpoint, 

192 user=user, 

193 email=email, 

194 account_type=account_type, 

195 first_name=first_name, 

196 last_name=last_name, 

197 country_code=country_code, 

198 currency_code=currency_code, 

199 ) 

200 

201 if isinstance(create_obj, StripeErrorContext): 

202 return create_obj.to_dict() 

203 

204 stripe_acc: AccountDTO = create_obj 

205 account_id = stripe_acc.account_id 

206 if IS_TRACE: 

207 msg = ( 

208 f"Created Stripe account {account_id} for user " 

209 f"{user_id} with response:\n{stripe_acc.debug_str()}" 

210 ) 

211 LOG().trace(msg) 

212 

213 # we dont have to update the db, create_onboard_link will handle that. 

214 link_response = self._create_onboard_link_response( 

215 endpoint=endpoint, 

216 user=user, 

217 account_id=account_id, 

218 overwrite=overwrite, 

219 error_code=error_code, 

220 ) 

221 

222 if not link_response.is_error: 

223 if IS_DEBUG: 

224 msg = f"User {user_id} already has an account, returning existing onboard link." 

225 LOG().debug(msg) 

226 

227 link = link_response.account_link 

228 return StripeCreateAccountResponseSchema( 

229 uid=user_id, 

230 url=link.url, 

231 expires_at=link.expires_at, 

232 account_id=link.account_id, 

233 ) 

234 

235 # handle error 

236 err = link_response.error 

237 assert err is not None # narrowing 

238 

239 msg = ( 

240 f"Exception creating new account for User ({user_id}/{account_id}), " 

241 f"creating new account.\n\tError:{err}" 

242 ) 

243 LOG().error(msg) 

244 return err.to_dict() 

245 

246 def callable_refresh_account( 

247 self, 

248 req: https_fn.CallableRequest[Any], 

249 ) -> StripeRefreshAccountResponseSchema | ErrorSchema: 

250 request = AppRequest.callable(req) 

251 endpoint = request.endpoint or "(internal)refresh_account" 

252 refresh_data: StripeRefreshAccountRequestAdapter | None = None 

253 try: 

254 refresh_data = StripeRefreshAccountRequestAdapter.from_callable(req) 

255 refresh_data.validate() 

256 except AppError as e: 

257 msg = f"Refresh Account Request validation error: {e}\n\tRequest: {request!s}" 

258 LOG().error(msg) 

259 error = StripeErrorContext.from_code( 

260 endpoint=endpoint, 

261 error_code=AppPaymentErrorCode.INVALID_REQUEST, 

262 cause=msg, 

263 error=e, 

264 ) 

265 return error.to_dict() 

266 

267 assert refresh_data is not None # narrowing 

268 

269 validated = self._validate_account( 

270 endpoint=endpoint, 

271 uid=refresh_data.uid, 

272 account_must_exist=True, 

273 ) 

274 

275 if validated.is_error: 

276 assert validated.error is not None # narrowing 

277 return validated.error.to_dict() 

278 

279 user = validated.user 

280 settings = validated.account_settings 

281 assert settings is not None # narrowing 

282 

283 user.update_stripe_account( 

284 account_id=refresh_data.stripe_account_id, 

285 account_type=refresh_data.account_type, 

286 currency_code=settings.currency_code, 

287 ) 

288 

289 try: 

290 self.link_handler.create_onboard_link( 

291 endpoint=endpoint, 

292 user=user, 

293 ) 

294 return self._build_refresh_response(user) 

295 except Exception as e: 

296 msg = f"Failed to refresh stripe account for uid={refresh_data.uid}\n\tError:{e}" 

297 error = StripeErrorContext.from_code( 

298 endpoint=endpoint, 

299 error_code=AppPaymentErrorCode.LINK_CREATE_FAILED, 

300 cause=msg, 

301 error=e, 

302 ) 

303 return error.to_dict() 

304 

305 def callable_upgrade_customer( 

306 self, 

307 req: https_fn.CallableRequest[Any], 

308 ) -> StripeUpgradeCustomerResponseSchema | ErrorSchema: 

309 request = AppRequest.callable(req) 

310 endpoint = request.endpoint 

311 

312 # Guard: Validate request data 

313 account_data: StripeUpgradeCustomerRequestAdapter | None = None 

314 try: 

315 account_data = StripeUpgradeCustomerRequestAdapter.from_callable(req) 

316 account_data.validate() 

317 except AppError as e: 

318 msg = f"Upgrade Customer Request validation error: {e}\n\tRequest: {request!s}" 

319 error = StripeErrorContext.from_code( 

320 endpoint="(internal)create_account", 

321 error_code=AppPaymentErrorCode.INVALID_REQUEST, 

322 cause=msg, 

323 error=e, 

324 ) 

325 return error.to_dict() 

326 

327 uid = account_data.uid 

328 account_type = account_data.account_type 

329 first_name = account_data.first_name 

330 last_name = account_data.last_name 

331 country_code = account_data.country_code 

332 currency_code = account_data.currency_code 

333 

334 validated = self._validate_account( 

335 endpoint=endpoint, 

336 uid=uid, 

337 account_must_exist=False, 

338 ) 

339 

340 if validated.is_error: 

341 assert validated.error is not None # narrowing 

342 return validated.error.to_dict() 

343 

344 if validated.is_account: 

345 # NOTE: the user must contact support to change/delete accounts 

346 # NOTE: as there are some manual steps that are required. 

347 # NOTE: so return an error with instructions 

348 msg = f"User {uid} already has an account, cannot upgrade customer." 

349 LOG().error(msg) 

350 error_ctx = StripeErrorContext.from_code( 

351 endpoint=endpoint, 

352 error_code=AppPaymentErrorCode.ACCOUNT_UPGRADE_EXISTING, 

353 cause=msg, 

354 ) 

355 return error_ctx.to_dict() 

356 

357 user = validated.user 

358 user_id = user.doc_id 

359 

360 email = account_data.email or user.email 

361 error_code: AppPaymentErrorCode = AppPaymentErrorCode.ACCOUNT_UPGRADE_FAILED 

362 

363 account_id: str | None = None 

364 try: 

365 account_id = self._upgrade_customer( 

366 endpoint=endpoint, 

367 user=validated.user, 

368 first_name=first_name, 

369 last_name=last_name, 

370 currency_code=currency_code, 

371 country_code=country_code, 

372 account_type=account_type, 

373 email=email, 

374 ) 

375 except Exception as err: 

376 msg = f"Failed to upgrade customer for uid={uid}\n\tError:{err}" 

377 LOG().error(msg) 

378 error_ctx = StripeErrorContext.from_code( 

379 endpoint=endpoint, 

380 error_code=AppPaymentErrorCode.ACCOUNT_UPGRADE_FAILED, 

381 cause=msg, 

382 error=err, 

383 ) 

384 return error_ctx.to_dict() 

385 

386 # we have to update the user model to 'promote' to an account 

387 # we delay updating the db, because create_onboard_link_response will do that 

388 

389 user.update_stripe_account( 

390 account_id=account_id, 

391 account_type=account_type, 

392 country_code=country_code, 

393 currency_code=currency_code, 

394 email=email, 

395 name=f"{first_name} {last_name}", 

396 ) 

397 

398 # now generate the link. 

399 link_response = self._create_onboard_link_response( 

400 endpoint=endpoint, 

401 user=user, 

402 account_id=account_id, 

403 error_code=error_code, 

404 overwrite=True, 

405 ) 

406 

407 if not link_response.is_error: 

408 if IS_DEBUG: 

409 msg = f"User {user_id} already has an account, returning existing onboard link." 

410 LOG().debug(msg) 

411 

412 link = link_response.account_link 

413 return StripeCreateAccountResponseSchema( 

414 uid=user_id, 

415 url=link.url, 

416 expires_at=link.expires_at, 

417 account_id=link.account_id, 

418 ) 

419 

420 # handle error 

421 link_err = link_response.error 

422 assert link_err is not None # narrowing 

423 

424 msg = ( 

425 f"Exception creating new account for User ({user_id}/{account_id}), " 

426 f"creating new account.\n\tError:{link_err}" 

427 ) 

428 LOG().error(msg) 

429 return link_err.to_dict() 

430 

431 # ---------------------------------------------------------------------------------------------- 

432 # MISCELLANEOUS PUBLIC METHODS 

433 # ---------------------------------------------------------------------------------------------- 

434 

435 def refresh_account( 

436 self, 

437 endpoint: str, 

438 user: UserWrapper, 

439 ) -> None: 

440 """Refresh Stripe account info and return structured response.""" 

441 uid = user.doc_id 

442 

443 settings = user.stripe_settings 

444 if settings is None or not StripeGuard.is_account(settings): 

445 msg = f"User {uid} does not have valid stripe account settings, cannot refresh." 

446 LOG().error(msg) 

447 raise ValueError(msg) 

448 

449 account_id = settings.account_id 

450 if IS_DEBUG: 

451 LOG().debug(f"Refreshing Stripe account {account_id} for user {uid}") 

452 

453 acct = self.proxy.get_account(endpoint=endpoint, account_id=account_id) 

454 if acct is None: 

455 msg = f"Failed to retrieve account info for account {account_id} during refresh" 

456 LOG().error(msg) 

457 error = StripeErrorContext.from_code( 

458 endpoint=endpoint, 

459 error_code=AppPaymentErrorCode.ACCOUNT_NOT_FOUND, 

460 cause=msg, 

461 ) 

462 raise AppError.from_context(error) 

463 

464 # Parse account type, fallback to EXPRESS if invalid 

465 acct_type = StripeUtil.cvt_account_type(acct.account_type) 

466 if acct_type is None: 

467 msg = ( 

468 f"Failed to parse account type {acct_type} for account {account_id}, using express" 

469 ) 

470 LOG().warning(msg) 

471 acct_type = StripeAccountType.EXPRESS 

472 

473 if acct_type == StripeAccountType.STANDARD: 

474 # PARKED: Stripe Standard accounts are complex, but may be required in the future. 

475 msg = f"Stripe standard accounts not currently supported, falling back to express for account {account_id}" 

476 LOG().error(msg) 

477 error = StripeErrorContext.from_code( 

478 endpoint=endpoint, 

479 error_code=AppPaymentErrorCode.ACCOUNT_NOT_FOUND, 

480 cause=msg, 

481 ) 

482 raise AppError.from_context(error) 

483 

484 # Build base response with required fields 

485 onboard_result = acct.onboard_result 

486 disabled = acct.disabled 

487 closed = acct.closed 

488 name = acct.name 

489 currency: StripeCurrencyCode | None = StripeUtil.cvt_stripe_currency(acct.currency) 

490 country: StripeCountryCode | None = StripeUtil.cvt_stripe_country(acct.country) 

491 account_id = acct.account_id 

492 

493 # Update user settings with refreshed data 

494 user.update_stripe_account( 

495 account_id=account_id, 

496 account_type=acct_type, 

497 onboard_result=onboard_result, 

498 stripe_disabled=disabled, 

499 stripe_closed=closed, 

500 stripe_disabled_reason=acct.disabled_reason, 

501 # preserve existing name if parsing failed 

502 name=name or settings.name, 

503 # preserve existing currency if parsing failed 

504 currency_code=currency or settings.currency_code, 

505 # preserve existing country if parsing failed 

506 country_code=country or settings.country_code, 

507 ) 

508 

509 def create_customer( 

510 self, 

511 endpoint: str, 

512 user: UserWrapper, 

513 alt_currency_code: StripeCurrencyCode, 

514 email: str | None = None, 

515 name: str | None = None, 

516 tokens: tuple[str, str] | None = None, 

517 ) -> str: 

518 settings = user.stripe_settings 

519 if settings is not None: 

520 return settings.customer_id 

521 

522 uid = user.doc_id 

523 actual_name = name or user.model.contact_name 

524 actual_email = email or user.email 

525 country_code = alt_currency_code.fallback_country_code 

526 

527 stripe_customer = self.proxy.create_customer( 

528 uid=uid, 

529 endpoint=endpoint, 

530 email=actual_email, 

531 name=actual_name, 

532 country_code=country_code, 

533 tokens=tokens, 

534 ) 

535 

536 customer_id = stripe_customer.id 

537 user.create_stripe_customer( 

538 customer_id=customer_id, 

539 country_code=country_code, 

540 currency_code=alt_currency_code, 

541 name=actual_name, 

542 email=actual_email, 

543 ) 

544 

545 if IS_DEBUG: 

546 LOG().debug(f"Created stripe customer {customer_id} for user {uid}") 

547 

548 return customer_id 

549 

550 # ======================================================================== 

551 # HELPERS 

552 # ======================================================================== 

553 

554 def _create_account( 

555 self, 

556 endpoint: str, 

557 user: UserWrapper, 

558 email: str, 

559 account_type: StripeAccountType, 

560 first_name: str, 

561 last_name: str, 

562 country_code: StripeCountryCode, 

563 currency_code: StripeCurrencyCode, 

564 ) -> StripeErrorContext | AccountDTO: 

565 

566 user_id = user.doc_id 

567 error_code: AppPaymentErrorCode = AppPaymentErrorCode.ACCOUNT_CREATE_FAILED 

568 stripe_acc: AccountDTO 

569 try: 

570 stripe_acc = self.proxy.create_account( 

571 uid=user_id, 

572 email=email, 

573 endpoint=endpoint, 

574 account_type=account_type, 

575 country_code=country_code, 

576 currency_code=currency_code, 

577 first_name=first_name, 

578 last_name=last_name, 

579 ) 

580 

581 except Exception as e: 

582 msg = f"Failed to create stripe account for user {user_id}\n\tError:{e}" 

583 LOG().error(msg) 

584 return StripeErrorContext.from_code( 

585 endpoint=endpoint, 

586 error_code=error_code, 

587 cause=msg, 

588 error=e, 

589 ) 

590 

591 if not stripe_acc.is_data_valid(): 

592 msg = ( 

593 f"Stripe account creation failed for user {user_id}: " 

594 f"Invalid account data:\n{stripe_acc.debug_str()}" 

595 ) 

596 LOG().error(msg) 

597 return StripeErrorContext.from_code( 

598 endpoint=endpoint, 

599 error_code=error_code, 

600 cause=msg, 

601 error=None, 

602 ) 

603 

604 # NOTE: we are using Stripe V2 customer accounts, so the account_id is also the customer_id 

605 account_id = stripe_acc.account_id 

606 user.create_stripe_account( 

607 onboard_state=StripeOnboardState.IN_PROGRESS, 

608 account_type=account_type, 

609 currency_code=currency_code, 

610 country_code=country_code, 

611 account_id=account_id, 

612 customer_id=account_id, 

613 ) 

614 

615 # update the payout schedule, log if it fails but dont block account creation, 

616 # support can update manually.. 

617 if not self._update_payout_schedule(account_id=account_id): 

618 msg = f"Failed to update payout schedule for account {account_id}" 

619 LOG().warning(msg) 

620 self.app_logger.payment_error( 

621 error_code=AppPaymentErrorCode.ACCOUNT_SCHEDULE_UPDATE_FAILED, 

622 doc_id=user_id, 

623 source="PaymentAccountHandler._update_payout_schedule", 

624 message=msg, 

625 ) 

626 

627 if IS_DEBUG: 

628 msg = ( 

629 f"Successfully created stripe account {account_id}" 

630 f" for user {user_id}.\n{stripe_acc.debug_str()}" 

631 ) 

632 LOG().debug(msg) 

633 

634 return stripe_acc 

635 

636 def _upgrade_customer( 

637 self, 

638 endpoint: str, 

639 user: UserWrapper, 

640 first_name: str, 

641 last_name: str, 

642 currency_code: StripeCurrencyCode, 

643 country_code: StripeCountryCode, 

644 account_type: StripeAccountType, 

645 email: str | None = None, 

646 ) -> str: 

647 user_id = user.doc_id 

648 settings = user.stripe_settings 

649 if settings is None: 

650 raise ValueError(f"User {user_id} does not have an account to upgrade") 

651 

652 customer_id = settings.customer_id 

653 actual_email = email or user.email 

654 

655 acct_dto = self.proxy.upgrade_customer( 

656 endpoint=endpoint, 

657 uid=user_id, 

658 customer_acct=customer_id, 

659 account_type=account_type, 

660 email=actual_email, 

661 country_code=country_code, 

662 currency_code=currency_code, 

663 tokens=(first_name, last_name), 

664 ) 

665 

666 account_id = acct_dto.account_id 

667 if account_id != customer_id: 

668 msg = ( 

669 f"Inconsitent identifiers during upgrade, " 

670 f"account_id={account_id}, customer_id={customer_id} for user {user_id}" 

671 ) 

672 LOG().error(msg) 

673 error = StripeErrorContext.from_code( 

674 endpoint=endpoint, 

675 error_code=AppPaymentErrorCode.ACCOUNT_UPGRADE_FAILED, 

676 cause=msg, 

677 ) 

678 raise AppError.from_context(error) 

679 

680 if IS_DEBUG: 

681 msg = f"Upgraded customer {customer_id} to account {account_id} for user {user_id}" 

682 LOG().debug(msg) 

683 

684 return account_id 

685 

686 def _validate_account( 

687 self, 

688 endpoint: str, 

689 uid: str, 

690 account_must_exist: bool = False, 

691 ) -> _ValidatedAccount: 

692 user: UserWrapper 

693 try: 

694 user = self.get_user_by_id(endpoint=endpoint, uid=uid) 

695 except Exception as e: 

696 msg = f"Failed to get user for uid={uid}\n\tError:{e}" 

697 LOG().error(msg) 

698 return _ValidatedAccount.from_error( 

699 StripeErrorContext.from_code( 

700 endpoint=endpoint, 

701 error_code=AppPaymentErrorCode.ACCOUNT_UPDATE_FAILED, 

702 cause=msg, 

703 error=e, 

704 ), 

705 ) 

706 

707 settings = user.stripe_settings 

708 customer_settings: StripeCustomerModel | None = None 

709 account_settings: StripeAccountModel | None = None 

710 

711 if account_must_exist and (settings is None or not StripeGuard.is_account(settings)): 

712 msg = f"User {uid} does not have valid stripe account settings" 

713 LOG().error(msg) 

714 return _ValidatedAccount.from_error( 

715 user=user, 

716 error=StripeErrorContext.from_code( 

717 endpoint=endpoint, 

718 error_code=AppPaymentErrorCode.ACCOUNT_UPDATE_FAILED, 

719 cause=msg, 

720 ), 

721 ) 

722 

723 if settings is not None: 

724 if StripeGuard.is_account(settings): 

725 account_settings = settings 

726 elif StripeGuard.is_customer(settings): 

727 customer_settings = settings 

728 

729 return _ValidatedAccount.success( 

730 user=user, 

731 account_settings=account_settings, 

732 customer_settings=customer_settings, 

733 ) 

734 

735 def _create_onboard_link_response( 

736 self, 

737 endpoint: str, 

738 user: UserWrapper, 

739 account_id: str, 

740 overwrite: bool, 

741 error_code: AppPaymentErrorCode = AppPaymentErrorCode.LINK_CREATE_FAILED, 

742 ) -> _LinkResponse: 

743 uid = user.doc_id 

744 

745 try: 

746 link = self.link_handler.create_onboard_link( 

747 endpoint=endpoint, 

748 user=user, 

749 overwrite=overwrite, 

750 ) 

751 account_link = _AccountLink( 

752 uid=uid, 

753 url=link["url"], 

754 expires_at=link["expires_at"], 

755 account_id=account_id, 

756 ) 

757 return _LinkResponse.success(account_link) 

758 except Exception as e: 

759 msg = f"Failed to create onboard link for new account {account_id} for user {uid}\n\tError:{e}" 

760 LOG().error(msg) 

761 error = StripeErrorContext.from_code( 

762 endpoint=endpoint, 

763 error_code=error_code, 

764 cause=msg, 

765 error=e, 

766 ) 

767 return _LinkResponse.from_error(error) 

768 

769 def _update_payout_schedule(self, account_id: str) -> bool: 

770 client = self.proxy.client 

771 

772 try: 

773 client.v1.balance_settings.update( 

774 { 

775 "payments": { 

776 "payouts": { 

777 "schedule": { 

778 "interval": "manual", 

779 } 

780 }, 

781 } 

782 }, 

783 options=self.proxy.request_options(account_id=account_id), 

784 ) 

785 if IS_DEBUG: 

786 LOG().debug(f"Updated payout schedule to manual for account {account_id}") 

787 

788 return True 

789 

790 except Exception as err: 

791 LOG().error( 

792 f"Failed to update payout schedule for account {account_id}\n\tError: {err!s}" 

793 ) 

794 return False 

795 

796 def _build_refresh_response( 

797 self, 

798 user: UserWrapper, 

799 ) -> StripeRefreshAccountResponseSchema: 

800 settings = user.stripe_settings 

801 uid = user.doc_id 

802 

803 if not StripeGuard.is_account(settings): 

804 msg = f"User {uid} has invalid stripe settings, cannot build refresh response." 

805 LOG().error(msg) 

806 raise ValueError(msg) 

807 

808 account_id = settings.account_id 

809 account_type = settings.account_type 

810 onboard_state = settings.onboard_state 

811 disabled = settings.stripe_disabled 

812 disabled_reason = settings.stripe_disabled_reason 

813 closed = settings.stripe_closed 

814 currency_code = settings.currency_code 

815 country_code = settings.country_code 

816 

817 onboard_result_message = onboard_state.label 

818 if disabled_reason is not None: 

819 onboard_result_message += f" - {disabled_reason}" 

820 

821 # Build base response with required fields 

822 data: StripeRefreshAccountResponseSchema = { 

823 "uid": uid, 

824 "account_id": str(account_id), 

825 "name": settings.name, 

826 "account_type": account_type, 

827 "onboard_result": onboard_state.value, 

828 "onboard_result_message": onboard_result_message, 

829 "disabled": disabled, 

830 "closed": closed, 

831 "currency_code": currency_code, 

832 "country_code": country_code, 

833 } 

834 

835 # Add optional fields only if they have non-None values 

836 if settings.stripe_disabled_reason is not None: 

837 data["disabled_reason"] = settings.stripe_disabled_reason 

838 

839 return data 

840 

841 

842# ---------------------------------------------------------------------------------------------- 

843# ---------------------------------------------------------------------------------------------- 

844# PRIVATE CLASSES 

845# ---------------------------------------------------------------------------------------------- 

846# ---------------------------------------------------------------------------------------------- 

847 

848 

849@dataclass(frozen=True, kw_only=True) 

850class _AccountLink: 

851 uid: str 

852 url: str 

853 expires_at: float 

854 account_id: str 

855 

856 

857@dataclass(frozen=True, kw_only=True) 

858class _LinkResponse: 

859 _account_link: _AccountLink | None 

860 error: StripeErrorContext | None 

861 

862 @classmethod 

863 def success(cls, account_link: _AccountLink) -> _LinkResponse: 

864 return cls(_account_link=account_link, error=None) 

865 

866 @classmethod 

867 def from_error(cls, error: StripeErrorContext) -> _LinkResponse: 

868 return cls(_account_link=None, error=error) 

869 

870 @property 

871 def is_error(self) -> bool: 

872 return self.error is not None 

873 

874 @property 

875 def is_success(self) -> bool: 

876 return self._account_link is not None 

877 

878 @property 

879 def account_link(self) -> _AccountLink: 

880 if self._account_link is None: 

881 raise ValueError("No account link associated with this response") 

882 return self._account_link 

883 

884 

885@dataclass(frozen=True, kw_only=True) 

886class _ValidatedAccount: 

887 _user: UserWrapper | None 

888 account_settings: StripeAccountModel | None 

889 customer_settings: StripeCustomerModel | None 

890 error: StripeErrorContext | None = None 

891 

892 @classmethod 

893 def success( 

894 cls, 

895 user: UserWrapper, 

896 account_settings: StripeAccountModel | None, 

897 customer_settings: StripeCustomerModel | None, 

898 ) -> _ValidatedAccount: 

899 return cls( 

900 _user=user, 

901 account_settings=account_settings, 

902 customer_settings=customer_settings, 

903 ) 

904 

905 @classmethod 

906 def from_error( 

907 cls, 

908 error: StripeErrorContext, 

909 user: UserWrapper | None = None, 

910 ) -> _ValidatedAccount: 

911 return cls( 

912 _user=user, 

913 account_settings=None, 

914 customer_settings=None, 

915 error=error, 

916 ) 

917 

918 @property 

919 def is_success(self) -> bool: 

920 return self.error is None 

921 

922 @property 

923 def is_error(self) -> bool: 

924 return self.error is not None 

925 

926 @property 

927 def is_customer(self) -> bool: 

928 return self.customer_settings is not None 

929 

930 @property 

931 def is_account(self) -> bool: 

932 return self.account_settings is not None 

933 

934 @property 

935 def user(self) -> UserWrapper: 

936 if self._user is None: 

937 raise ValueError("No user associated with this account") 

938 return self._user