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
« 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#
13from __future__ import annotations
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
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
59__all__ = ["PaymentAccountHandler"]
62class PaymentAccountHandler(BasePaymentHandler, ErrorMixin, UserMixin, ServiceProvider):
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
75 super().__init__(
76 db_manager=db_manager,
77 backend_manager=backend_manager,
78 service_manager=service_manager,
79 )
81 # ----------------------------------------------------------------------------------------------
82 # CALLABLE REQUESTS
83 # ----------------------------------------------------------------------------------------------
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.
92 Returns:
93 StripeCreateAccountResponseSchema on success
94 ErrorSchema on validation or creation failure
96 """
97 request = AppRequest.callable(req)
98 endpoint = request.endpoint
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()
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
124 validated = self._validate_account(
125 endpoint=endpoint,
126 uid=uid,
127 account_must_exist=False,
128 )
130 if validated.is_error:
131 assert validated.error is not None # narrowing
132 return validated.error.to_dict()
134 user = validated.user
135 user_id = user.doc_id
136 settings = validated.account_settings
138 email = account_data.email or user.email
139 error_code: AppPaymentErrorCode = AppPaymentErrorCode.ACCOUNT_CREATE_FAILED
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 )
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)
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 )
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)
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 )
201 if isinstance(create_obj, StripeErrorContext):
202 return create_obj.to_dict()
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)
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 )
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)
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 )
235 # handle error
236 err = link_response.error
237 assert err is not None # narrowing
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()
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()
267 assert refresh_data is not None # narrowing
269 validated = self._validate_account(
270 endpoint=endpoint,
271 uid=refresh_data.uid,
272 account_must_exist=True,
273 )
275 if validated.is_error:
276 assert validated.error is not None # narrowing
277 return validated.error.to_dict()
279 user = validated.user
280 settings = validated.account_settings
281 assert settings is not None # narrowing
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 )
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()
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
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()
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
334 validated = self._validate_account(
335 endpoint=endpoint,
336 uid=uid,
337 account_must_exist=False,
338 )
340 if validated.is_error:
341 assert validated.error is not None # narrowing
342 return validated.error.to_dict()
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()
357 user = validated.user
358 user_id = user.doc_id
360 email = account_data.email or user.email
361 error_code: AppPaymentErrorCode = AppPaymentErrorCode.ACCOUNT_UPGRADE_FAILED
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()
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
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 )
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 )
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)
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 )
420 # handle error
421 link_err = link_response.error
422 assert link_err is not None # narrowing
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()
431 # ----------------------------------------------------------------------------------------------
432 # MISCELLANEOUS PUBLIC METHODS
433 # ----------------------------------------------------------------------------------------------
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
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)
449 account_id = settings.account_id
450 if IS_DEBUG:
451 LOG().debug(f"Refreshing Stripe account {account_id} for user {uid}")
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)
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
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)
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
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 )
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
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
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 )
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 )
545 if IS_DEBUG:
546 LOG().debug(f"Created stripe customer {customer_id} for user {uid}")
548 return customer_id
550 # ========================================================================
551 # HELPERS
552 # ========================================================================
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:
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 )
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 )
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 )
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 )
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 )
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)
634 return stripe_acc
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")
652 customer_id = settings.customer_id
653 actual_email = email or user.email
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 )
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)
680 if IS_DEBUG:
681 msg = f"Upgraded customer {customer_id} to account {account_id} for user {user_id}"
682 LOG().debug(msg)
684 return account_id
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 )
707 settings = user.stripe_settings
708 customer_settings: StripeCustomerModel | None = None
709 account_settings: StripeAccountModel | None = None
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 )
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
729 return _ValidatedAccount.success(
730 user=user,
731 account_settings=account_settings,
732 customer_settings=customer_settings,
733 )
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
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)
769 def _update_payout_schedule(self, account_id: str) -> bool:
770 client = self.proxy.client
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}")
788 return True
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
796 def _build_refresh_response(
797 self,
798 user: UserWrapper,
799 ) -> StripeRefreshAccountResponseSchema:
800 settings = user.stripe_settings
801 uid = user.doc_id
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)
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
817 onboard_result_message = onboard_state.label
818 if disabled_reason is not None:
819 onboard_result_message += f" - {disabled_reason}"
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 }
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
839 return data
842# ----------------------------------------------------------------------------------------------
843# ----------------------------------------------------------------------------------------------
844# PRIVATE CLASSES
845# ----------------------------------------------------------------------------------------------
846# ----------------------------------------------------------------------------------------------
849@dataclass(frozen=True, kw_only=True)
850class _AccountLink:
851 uid: str
852 url: str
853 expires_at: float
854 account_id: str
857@dataclass(frozen=True, kw_only=True)
858class _LinkResponse:
859 _account_link: _AccountLink | None
860 error: StripeErrorContext | None
862 @classmethod
863 def success(cls, account_link: _AccountLink) -> _LinkResponse:
864 return cls(_account_link=account_link, error=None)
866 @classmethod
867 def from_error(cls, error: StripeErrorContext) -> _LinkResponse:
868 return cls(_account_link=None, error=error)
870 @property
871 def is_error(self) -> bool:
872 return self.error is not None
874 @property
875 def is_success(self) -> bool:
876 return self._account_link is not None
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
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
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 )
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 )
918 @property
919 def is_success(self) -> bool:
920 return self.error is None
922 @property
923 def is_error(self) -> bool:
924 return self.error is not None
926 @property
927 def is_customer(self) -> bool:
928 return self.customer_settings is not None
930 @property
931 def is_account(self) -> bool:
932 return self.account_settings is not None
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