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
« 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
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
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
50__all__ = ["AppPaymentService"]
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
64 super().__init__(
65 backend_manager=backend_manager,
66 db_manager=db_manager,
67 service_manager=service_manager,
68 )
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
79 # ========================================================================
80 # SETUP
81 # ========================================================================
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
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 )
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 )
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
134 return self._webhook_handler
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
142 return self._link_handler
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
150 return self._account_handler
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
158 return self._charge_handler
160 @property
161 def proxy(self) -> AppStripeProxy:
162 return self._proxy
164 # ========================================================================
165 # TRIGGERS/WEBHOOKs
166 # ========================================================================
168 def handle_refresh_webhook(self, req: flask.Request) -> flask.Response:
169 return self.webhook_handler.handle_refresh(req)
171 def handle_return_webhook(self, req: flask.Request) -> flask.Response:
172 return self.webhook_handler.handle_return(req)
174 def create_account(
175 self,
176 req: https_fn.CallableRequest[Any],
177 ) -> StripeCreateAccountResponseSchema | ErrorSchema:
178 return self.account_handler.callable_create_account(req)
180 def refresh_account(
181 self,
182 req: https_fn.CallableRequest[Any],
183 ) -> StripeRefreshAccountResponseSchema | ErrorSchema:
184 return self.account_handler.callable_refresh_account(req)
186 def upgrade_customer(
187 self,
188 req: https_fn.CallableRequest[Any],
189 ) -> StripeUpgradeCustomerResponseSchema | ErrorSchema:
190 return self.account_handler.callable_upgrade_customer(req)
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)
198 def create_charge(
199 self,
200 req: https_fn.CallableRequest[Any],
201 ) -> PaymentCreateResponseSchema | ErrorSchema:
202 return self.charge_handler.callable_create_charge(req)
204 def confirm_charge(
205 self,
206 req: https_fn.CallableRequest[Any],
207 ) -> PaymentConfirmResponseSchema | ErrorSchema:
208 return self.charge_handler.callable_confirm_charge(req)
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)
218 # ========================================================================
219 # CRONS
220 # ========================================================================
222 @cron_decorator(job_type=AppJobType.CR_PAYMENT_UNPROCESSED)
223 def cron_payment_unprocessed(self) -> CronResult:
224 job_type = AppJobType.CR_PAYMENT_UNPROCESSED
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()
234 @cron_decorator(job_type=AppJobType.CR_REFUND_UNPROCESSED)
235 def cron_refund_unprocessed(self) -> CronResult:
236 job_type = AppJobType.CR_REFUND_UNPROCESSED
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()
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()
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()
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()
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()
290 # ========================================================================
291 # CRON TASKS
292 # ========================================================================
294 def process_unprocessed_payment(
295 self,
296 payment: PledgeWrapper,
297 ) -> JobResult[PledgeWrapper]:
298 return self.charge_handler.process_unprocessed_payment(payment)
300 def reauthorize_payment(
301 self,
302 payment: PledgeWrapper,
303 ) -> JobResult[PledgeWrapper]:
304 return self.charge_handler.reauthorize_payment(payment)
306 def capture_payment(
307 self,
308 payment: PledgeWrapper,
309 ) -> JobResult[PledgeWrapper]:
310 return self.charge_handler.capture_payment(payment)
312 def transfer_payment(
313 self,
314 payment: PledgeWrapper,
315 ) -> JobResult[PledgeWrapper]:
316 return self.charge_handler.transfer_payment(payment)
318 def refund_payment(
319 self,
320 payment: PledgeWrapper,
321 ) -> JobResult[PledgeWrapper]:
322 return self.charge_handler.refund_payment(payment)