Coverage for functions \ flipdare \ service \ payments \ app_payment_service.py: 66%

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 

15import flask 

16import stripe 

17from typing import TYPE_CHECKING, Any 

18from functools import partial 

19from firebase_functions import https_fn 

20from flipdare.generated import ( 

21 PaymentCreateResponseSchema, 

22 PaymentConfirmResponseSchema, 

23 StripeCreateAccountResponseSchema, 

24 StripeRefreshAccountResponseSchema, 

25 StripeUpgradeCustomerResponseSchema, 

26 StripeLinkResponseSchema, 

27 AppJobType, 

28 ErrorSchema, 

29) 

30from flipdare.payments.app_stripe_proxy import AppStripeProxy 

31from flipdare.payments.app_stripe_config import AppStripeConfig 

32from flipdare.result.job_result import JobResult 

33from flipdare.service._error_mixin import ErrorMixin 

34from flipdare.service._service_provider import ServiceProvider 

35from flipdare.service._user_mixin import UserMixin 

36from flipdare.service.payments._payment_account_handler import PaymentAccountHandler 

37from flipdare.service.payments._payment_charge_handler import PaymentChargeHandler 

38from flipdare.service.payments._payment_link_handler import PaymentLinkHandler 

39from flipdare.service.payments._payment_webhook_handler import PaymentWebhookHandler 

40from flipdare.app_types import CronResult 

41from flipdare.core.cron_decorator import cron_decorator 

42from flipdare.service.core.cron_processor import CronConfig, CronProcessor 

43from flipdare.wrapper.payment.pledge_wrapper import PledgeWrapper 

44 

45if TYPE_CHECKING: 

46 from flipdare.manager.db_manager import DbManager 

47 from flipdare.manager.backend_manager import BackendManager 

48 from flipdare.manager.service_manager import ServiceManager 

49 

50__all__ = ["AppPaymentService"] 

51 

52 

53class AppPaymentService(ErrorMixin, UserMixin, ServiceProvider): 

54 def __init__( 

55 self, 

56 stripe_client: stripe.StripeClient | None = None, 

57 stripe_config: AppStripeConfig | None = None, 

58 db_manager: DbManager | None = None, 

59 backend_manager: BackendManager | None = None, 

60 service_manager: ServiceManager | None = None, 

61 ) -> None: 

62 from flipdare.payments.app_stripe_proxy import create_stripe_proxy 

63 

64 super().__init__( 

65 backend_manager=backend_manager, 

66 db_manager=db_manager, 

67 service_manager=service_manager, 

68 ) 

69 

70 self._proxy = create_stripe_proxy( 

71 stripe_config=stripe_config, 

72 stripe_client=stripe_client, 

73 ) 

74 self._link_handler: PaymentLinkHandler | None = None 

75 self._account_handler: PaymentAccountHandler | None = None 

76 self._charge_handler: PaymentChargeHandler | None = None 

77 self._webhook_handler: PaymentWebhookHandler | None = None 

78 

79 # ======================================================================== 

80 # SETUP 

81 # ======================================================================== 

82 

83 def _initialize(self) -> None: 

84 # because there are interdependencies between handlers, 

85 # our lazy initialization approach needs to initialize all handlers at once 

86 if ( 

87 self._link_handler is not None 

88 and self._account_handler is not None 

89 and self._charge_handler is not None 

90 and self._webhook_handler is not None 

91 ): 

92 return 

93 

94 if self._link_handler is None: 

95 self._link_handler = PaymentLinkHandler( 

96 proxy=self.proxy, 

97 db_manager=self.db_manager, 

98 backend_manager=self.backend_manager, 

99 service_manager=self.service_manager, 

100 ) 

101 

102 if self._account_handler is None: 

103 self._account_handler = PaymentAccountHandler( 

104 proxy=self.proxy, 

105 link_handler=self._link_handler, 

106 db_manager=self.db_manager, 

107 backend_manager=self.backend_manager, 

108 service_manager=self.service_manager, 

109 ) 

110 if self._charge_handler is None: 

111 self._charge_handler = PaymentChargeHandler( 

112 proxy=self.proxy, 

113 account_handler=self._account_handler, 

114 db_manager=self.db_manager, 

115 backend_manager=self.backend_manager, 

116 service_manager=self.service_manager, 

117 ) 

118 if self._webhook_handler is None: 

119 self._webhook_handler = PaymentWebhookHandler( 

120 proxy=self.proxy, 

121 account_handler=self._account_handler, 

122 link_handler=self._link_handler, 

123 db_manager=self.db_manager, 

124 backend_manager=self.backend_manager, 

125 service_manager=self.service_manager, 

126 ) 

127 

128 @property 

129 def webhook_handler(self) -> PaymentWebhookHandler: 

130 if self._webhook_handler is None: 

131 self._initialize() 

132 assert self._webhook_handler is not None 

133 

134 return self._webhook_handler 

135 

136 @property 

137 def link_handler(self) -> PaymentLinkHandler: 

138 if self._link_handler is None: 

139 self._initialize() 

140 assert self._link_handler is not None 

141 

142 return self._link_handler 

143 

144 @property 

145 def account_handler(self) -> PaymentAccountHandler: 

146 if self._account_handler is None: 

147 self._initialize() 

148 assert self._account_handler is not None 

149 

150 return self._account_handler 

151 

152 @property 

153 def charge_handler(self) -> PaymentChargeHandler: 

154 if self._charge_handler is None: 

155 self._initialize() 

156 assert self._charge_handler is not None 

157 

158 return self._charge_handler 

159 

160 @property 

161 def proxy(self) -> AppStripeProxy: 

162 return self._proxy 

163 

164 # ======================================================================== 

165 # TRIGGERS/WEBHOOKs 

166 # ======================================================================== 

167 

168 def handle_refresh_webhook(self, req: flask.Request) -> flask.Response: 

169 return self.webhook_handler.handle_refresh(req) 

170 

171 def handle_return_webhook(self, req: flask.Request) -> flask.Response: 

172 return self.webhook_handler.handle_return(req) 

173 

174 def create_account( 

175 self, 

176 req: https_fn.CallableRequest[Any], 

177 ) -> StripeCreateAccountResponseSchema | ErrorSchema: 

178 return self.account_handler.callable_create_account(req) 

179 

180 def refresh_account( 

181 self, 

182 req: https_fn.CallableRequest[Any], 

183 ) -> StripeRefreshAccountResponseSchema | ErrorSchema: 

184 return self.account_handler.callable_refresh_account(req) 

185 

186 def upgrade_customer( 

187 self, 

188 req: https_fn.CallableRequest[Any], 

189 ) -> StripeUpgradeCustomerResponseSchema | ErrorSchema: 

190 return self.account_handler.callable_upgrade_customer(req) 

191 

192 def create_onboard_link( 

193 self, 

194 req: https_fn.CallableRequest[Any], 

195 ) -> StripeLinkResponseSchema | ErrorSchema: 

196 return self.link_handler.callable_create_onboard_link(req) 

197 

198 def create_charge( 

199 self, 

200 req: https_fn.CallableRequest[Any], 

201 ) -> PaymentCreateResponseSchema | ErrorSchema: 

202 return self.charge_handler.callable_create_charge(req) 

203 

204 def confirm_charge( 

205 self, 

206 req: https_fn.CallableRequest[Any], 

207 ) -> PaymentConfirmResponseSchema | ErrorSchema: 

208 return self.charge_handler.callable_confirm_charge(req) 

209 

210 # NOTE: webhooks are currently not required since the stripe 

211 # flutter payment form handles 3ds auth. 

212 # def handle_charge_webhook( 

213 # self, 

214 # req: flask.Request 

215 # ) -> flask.Response: 

216 # return self.charge_handler.handle_charge_webhook(req) 

217 

218 # ======================================================================== 

219 # CRONS 

220 # ======================================================================== 

221 

222 @cron_decorator(job_type=AppJobType.CR_PAYMENT_UNPROCESSED) 

223 def cron_payment_unprocessed(self) -> CronResult: 

224 job_type = AppJobType.CR_PAYMENT_UNPROCESSED 

225 

226 config = CronConfig( 

227 job_type=job_type, 

228 job_name=job_type.value, 

229 query_fn=lambda: self.pledge_db.get_unprocessed_payments(), 

230 process_fn=lambda pledge: self.process_unprocessed_payment(pledge), 

231 ) 

232 return CronProcessor(config).process_result() 

233 

234 @cron_decorator(job_type=AppJobType.CR_REFUND_UNPROCESSED) 

235 def cron_refund_unprocessed(self) -> CronResult: 

236 job_type = AppJobType.CR_REFUND_UNPROCESSED 

237 

238 config = CronConfig( 

239 job_type=job_type, 

240 job_name=job_type.value, 

241 query_fn=lambda: self.pledge_db.get_unprocessed_refunds(), 

242 process_fn=lambda pledge: self.refund_payment(pledge), 

243 ) 

244 return CronProcessor(config).process_result() 

245 

246 @cron_decorator(job_type=AppJobType.CR_PAYMENT_REAUTHORIZE) 

247 def cron_reauthorize_charge(self) -> CronResult: 

248 job_type = AppJobType.CR_PAYMENT_REAUTHORIZE 

249 config = CronConfig( 

250 job_type=job_type, 

251 job_name=job_type.value, 

252 query_fn=partial(self.pledge_db.get_pledges_to_reauthorize), 

253 process_fn=lambda charge: self.reauthorize_payment(charge), 

254 ) 

255 return CronProcessor(config).process_result() 

256 

257 @cron_decorator(job_type=AppJobType.CR_PAYMENT_CAPTURE) 

258 def cron_capture_charge(self) -> CronResult: 

259 job_type = AppJobType.CR_PAYMENT_CAPTURE 

260 config = CronConfig( 

261 job_type=job_type, 

262 job_name=job_type.value, 

263 query_fn=partial(self.pledge_db.get_pledges_to_capture), 

264 process_fn=lambda charge: self.capture_payment(charge), 

265 ) 

266 return CronProcessor(config).process_result() 

267 

268 @cron_decorator(job_type=AppJobType.CR_PAYMENT_TRANSFER) 

269 def cron_transfer_charge(self) -> CronResult: 

270 job_type = AppJobType.CR_PAYMENT_TRANSFER 

271 config = CronConfig( 

272 job_type=job_type, 

273 job_name=job_type.value, 

274 query_fn=partial(self.pledge_db.get_pledges_to_transfer), 

275 process_fn=lambda charge: self.transfer_payment(charge), 

276 ) 

277 return CronProcessor(config).process_result() 

278 

279 @cron_decorator(job_type=AppJobType.CR_PAYMENT_REFUND) 

280 def cron_refund_charge(self) -> CronResult: 

281 job_type = AppJobType.CR_PAYMENT_REFUND 

282 config = CronConfig( 

283 job_type=job_type, 

284 job_name=job_type.value, 

285 query_fn=partial(self.pledge_db.get_pledges_to_refund), 

286 process_fn=lambda charge: self.refund_payment(charge), 

287 ) 

288 return CronProcessor(config).process_result() 

289 

290 # ======================================================================== 

291 # CRON TASKS 

292 # ======================================================================== 

293 

294 def process_unprocessed_payment( 

295 self, 

296 payment: PledgeWrapper, 

297 ) -> JobResult[PledgeWrapper]: 

298 return self.charge_handler.process_unprocessed_payment(payment) 

299 

300 def reauthorize_payment( 

301 self, 

302 payment: PledgeWrapper, 

303 ) -> JobResult[PledgeWrapper]: 

304 return self.charge_handler.reauthorize_payment(payment) 

305 

306 def capture_payment( 

307 self, 

308 payment: PledgeWrapper, 

309 ) -> JobResult[PledgeWrapper]: 

310 return self.charge_handler.capture_payment(payment) 

311 

312 def transfer_payment( 

313 self, 

314 payment: PledgeWrapper, 

315 ) -> JobResult[PledgeWrapper]: 

316 return self.charge_handler.transfer_payment(payment) 

317 

318 def refund_payment( 

319 self, 

320 payment: PledgeWrapper, 

321 ) -> JobResult[PledgeWrapper]: 

322 return self.charge_handler.refund_payment(payment)