Coverage for functions \ flipdare \ service \ payments \ _payment_link_handler.py: 53%

125 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 

16from stripe import AccountLink as StripeAccountLink 

17from typing import Any 

18from firebase_functions import https_fn 

19from flipdare.app_log import LOG 

20from flipdare.constants import IS_DEBUG 

21from flipdare.message.error_message import ErrorMessage 

22from flipdare.error import ( 

23 AppError, 

24 StripeErrorContext, 

25) 

26from flipdare.generated.shared.app_payment_error_code import AppPaymentErrorCode 

27from flipdare.service._error_mixin import ErrorMixin 

28from flipdare.generated import ( 

29 ErrorSchema, 

30 StripeAccountType, 

31 StripeLinkResponseSchema, 

32) 

33from flipdare.payments.app_stripe_proxy import AppStripeProxy 

34from flipdare.payments.app_stripe_config import ( 

35 StripeLinkInfo, 

36) 

37from flipdare.request import ( 

38 StripeRefreshAccountRequestAdapter, 

39) 

40from flipdare.service._service_provider import ServiceProvider 

41from flipdare.service._user_mixin import UserMixin 

42from flipdare.service.payments._base_payment_handler import BasePaymentHandler 

43from flipdare.payments.core.stripe_guard import StripeGuard 

44from flipdare.wrapper.user_wrapper import UserWrapper 

45 

46if TYPE_CHECKING: 

47 from flipdare.manager.service_manager import ServiceManager 

48 from flipdare.manager.backend_manager import BackendManager 

49 from flipdare.manager.db_manager import DbManager 

50 

51__all__ = ["PaymentLinkHandler"] 

52 

53 

54class PaymentLinkHandler(BasePaymentHandler, ErrorMixin, UserMixin, ServiceProvider): 

55 def __init__( 

56 self, 

57 proxy: AppStripeProxy, 

58 db_manager: DbManager | None = None, 

59 backend_manager: BackendManager | None = None, 

60 service_manager: ServiceManager | None = None, 

61 ) -> None: 

62 self.proxy = proxy 

63 super().__init__( 

64 db_manager=db_manager, 

65 backend_manager=backend_manager, 

66 service_manager=service_manager, 

67 ) 

68 

69 # ---------------------------------------------------------------------------------------------- 

70 # CALLABLE REQUESTS 

71 # ---------------------------------------------------------------------------------------------- 

72 

73 def callable_create_onboard_link( 

74 self, 

75 req: https_fn.CallableRequest[Any], 

76 ) -> StripeLinkResponseSchema | ErrorSchema: 

77 request_adapter: StripeRefreshAccountRequestAdapter | None = None 

78 endpoint = req.raw_request.endpoint or "(internal)create_onboard_link" 

79 try: 

80 request_adapter = StripeRefreshAccountRequestAdapter.from_callable(req) 

81 request_adapter.validate() 

82 except AppError as e: 

83 cause = f"Onboard link request validation error: {e}\n\tRequest: {req!s}" 

84 LOG().error(cause) 

85 error = StripeErrorContext.from_code( 

86 endpoint=endpoint, 

87 error_code=AppPaymentErrorCode.INVALID_REQUEST, 

88 cause=cause, 

89 error=e, 

90 ) 

91 return error.to_dict() 

92 

93 uid = request_adapter.uid 

94 try: 

95 user = self.get_user_by_id(endpoint=request_adapter.endpoint, uid=uid) 

96 except Exception as e: 

97 msg = f"Failed to get user info for uid={uid}: {e}" 

98 LOG().error(msg) 

99 error = StripeErrorContext.from_code( 

100 endpoint=endpoint, 

101 error_code=AppPaymentErrorCode.INVALID_USER, 

102 cause=msg, 

103 error=e, 

104 ) 

105 return error.to_dict() 

106 

107 settings = user.stripe_settings 

108 if not StripeGuard.is_account(settings): 

109 msg = f"Unable to refresh URL for user {user.doc_id}: No stripe settings.." 

110 LOG().error(msg) 

111 error = StripeErrorContext.from_code( 

112 endpoint=endpoint, 

113 error_code=AppPaymentErrorCode.INVALID_SETTINGS, 

114 cause=msg, 

115 ) 

116 return error.to_dict() 

117 

118 account_id = request_adapter.stripe_account_id 

119 account_type = request_adapter.account_type 

120 country_code = settings.country_code 

121 currency_code = settings.currency_code 

122 

123 # we dont worry about changes, the db is updated when the link is created.. 

124 settings.update( 

125 account_id=account_id, 

126 country_code=country_code, 

127 currency_code=currency_code, 

128 account_type=account_type, 

129 ) 

130 

131 try: 

132 return self.create_onboard_link( 

133 endpoint=request_adapter.endpoint, 

134 user=user, 

135 # we want to force a new link to be created when refreshing, 

136 # since the old one is no longer valid 

137 overwrite=True, 

138 ) 

139 except Exception as e: 

140 msg = f"Failed to create onboard link for user {user.doc_id}\n\tError:{e}" 

141 LOG().error(msg) 

142 error = StripeErrorContext.from_code( 

143 endpoint=endpoint, 

144 error_code=AppPaymentErrorCode.LINK_CREATE_FAILED, 

145 cause=msg, 

146 error=e, 

147 ) 

148 return error.to_dict() 

149 

150 # ---------------------------------------------------------------------------------------------- 

151 # LINK REQUESTS 

152 # ---------------------------------------------------------------------------------------------- 

153 

154 def create_onboard_link( 

155 self, 

156 endpoint: str, 

157 user: UserWrapper, 

158 overwrite: bool = False, 

159 ) -> StripeLinkResponseSchema: 

160 account_link: StripeLinkResponseSchema | None = None 

161 settings = user.stripe_settings 

162 if not StripeGuard.is_account(settings): 

163 msg = f"User {user.doc_id} does not have a valid stripe account in settings.." 

164 LOG().error(msg) 

165 error = StripeErrorContext.from_code( 

166 endpoint=endpoint, 

167 error_code=AppPaymentErrorCode.INVALID_SETTINGS, 

168 cause=msg, 

169 ) 

170 raise AppError.from_context(error) 

171 

172 account_id = settings.account_id 

173 if not overwrite: 

174 account_link = self.get_stored_onboard_link(user) 

175 if account_link is not None: 

176 msg = f"Existing account link for {account_id} (force={overwrite}): {account_link}" 

177 LOG().debug(msg) 

178 return account_link 

179 

180 onboard_link = self._create_new_onboard_link( 

181 endpoint=endpoint, 

182 uid=user.doc_id, 

183 account_id=account_id, 

184 account_type=settings.account_type, 

185 ) 

186 

187 user.update_stripe_account( 

188 account_link=onboard_link["url"], 

189 account_link_expires=onboard_link["expires_at"], 

190 # existing settings, but required to avoid any promotion/demotion issues. 

191 account_id=account_id, 

192 account_type=settings.account_type, 

193 currency_code=settings.currency_code, 

194 ) 

195 

196 try: 

197 self.update_user( 

198 endpoint=endpoint, 

199 user=user, 

200 on_error_msg=ErrorMessage.STR_ACC_UPDATE_FAILED, 

201 ) 

202 

203 return StripeLinkResponseSchema( 

204 { 

205 "url": onboard_link["url"], 

206 "expires_at": onboard_link["expires_at"], 

207 "account_id": account_id, 

208 } 

209 ) 

210 except Exception as e: 

211 msg = f"Failed to save account link for {account_id} in db\n\tError:{e}" 

212 LOG().error(msg) 

213 error = StripeErrorContext.from_code( 

214 endpoint=endpoint, 

215 error_code=AppPaymentErrorCode.LINK_CREATE_FAILED, 

216 cause=msg, 

217 error=e, 

218 ) 

219 raise AppError.from_context(error) from e 

220 

221 def get_stored_onboard_link( 

222 self, 

223 user: UserWrapper, 

224 ) -> StripeLinkResponseSchema | None: 

225 stripe_settings = user.stripe_settings 

226 if not StripeGuard.is_account(stripe_settings): 

227 msg = f"User {user.doc_id} does not have valid stripe account settings.." 

228 LOG().error(msg) 

229 return None 

230 

231 is_active = stripe_settings.is_link_active 

232 if not is_active: 

233 if IS_DEBUG: 

234 msg = ( 

235 f"Existing link {user.doc_id} " 

236 f"({stripe_settings.account_link_expires}) expired or inactive." 

237 ) 

238 LOG().debug(msg) 

239 

240 return None 

241 

242 account_link = stripe_settings.account_link 

243 account_id = stripe_settings.account_id 

244 account_link_expires = stripe_settings.account_link_expires 

245 

246 if account_link is None or account_link_expires is None: 

247 msg = ( 

248 f"Missing fields for link for {user.doc_id}: " 

249 f"account_link={account_link}, account_id={account_id}, " 

250 f"account_link_expires={account_link_expires}" 

251 ) 

252 LOG().error(msg) 

253 return None 

254 

255 if IS_DEBUG: 

256 msg = ( 

257 f"Existing account link for user {user.doc_id}/{account_id} is active: " 

258 f"{account_link}, expires at {account_link_expires}" 

259 ) 

260 LOG().debug(msg) 

261 

262 link = StripeLinkResponseSchema( 

263 url=account_link, 

264 expires_at=account_link_expires, 

265 account_id=account_id, 

266 ) 

267 

268 if IS_DEBUG: 

269 msg = f"Existing link still active for {user.doc_id} : {link}" 

270 LOG().debug(msg) 

271 

272 return link 

273 

274 # ---------------------------------------------------------------------------------------------- 

275 # INTERNAL: LINKs 

276 # ---------------------------------------------------------------------------------------------- 

277 

278 def _create_new_onboard_link( 

279 self, 

280 endpoint: str, 

281 uid: str, 

282 account_id: str, 

283 account_type: StripeAccountType, 

284 ) -> StripeLinkResponseSchema: 

285 account_link: StripeAccountLink | None = None 

286 

287 try: 

288 info = StripeLinkInfo( 

289 uid=uid, 

290 stripe_account_id=account_id, 

291 account_type=account_type, 

292 ) 

293 account_link = self.proxy.create_onboard_link(endpoint, info) 

294 

295 except Exception as e: 

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

297 LOG().error(msg) 

298 error = StripeErrorContext.from_code( 

299 endpoint=endpoint, 

300 error_code=AppPaymentErrorCode.LINK_CREATE_FAILED, 

301 cause=msg, 

302 error=e, 

303 ) 

304 raise AppError.from_context(error) from e 

305 

306 return StripeLinkResponseSchema( 

307 url=account_link.url, 

308 expires_at=account_link.expires_at, 

309 account_id=account_id, 

310 )