Coverage for functions \ flipdare \ service \ pledge_service.py: 31%
88 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
14from flipdare.app_log import LOG
15from flipdare.constants import IS_DEBUG, IS_TRACE
16from flipdare.generated.shared.app_error_code import AppErrorCode
17from flipdare.result.app_result import AppResult
18from flipdare.service._error_mixin import ErrorMixin
19from flipdare.service._service_provider import ServiceProvider
20from flipdare.core.job_type_decorator import job_type_decorator
21from flipdare.result.job_result import JobResult
22from flipdare.core.trigger_decorator import trigger_decorator
23from flipdare.generated import AppJobType
24from flipdare.service.payments.app_payment_service import AppPaymentService
25from flipdare.generated.shared.firestore_collections import FirestoreCollections
26from flipdare.wrapper.backend.app_job_wrapper import AppJobWrapper
27from flipdare.wrapper.dare_wrapper import DareWrapper
28from flipdare.wrapper.payment.pledge_wrapper import PledgeWrapper
30__all__ = ["PledgeService"]
33_JT = AppJobType
34_COL = FirestoreCollections.PLEDGE
37class PledgeService(ErrorMixin, ServiceProvider):
39 def __init__(
40 self,
41 client: AppPaymentService | None = None,
42 ) -> None:
43 super().__init__()
45 self._client = client
47 @property
48 def client(self) -> AppPaymentService:
49 if self._client is None:
50 self._client = AppPaymentService()
51 return self._client
53 # ========================================================================
54 # TRIGGERS - Delegate to processors
55 # ========================================================================
57 @job_type_decorator(_JT.TR_PLEDGE)
58 @trigger_decorator(job_type=_JT.TR_PLEDGE, collection=_COL, wrapper_class=PledgeWrapper)
59 def trigger_pledge(
60 self,
61 job: AppJobWrapper,
62 *,
63 pledge: PledgeWrapper,
64 ) -> JobResult[PledgeWrapper]:
65 # NOTE: pledges are less frequent than other stats.
66 # NOTE: so it should be safe to do the stats update here, rather than delegating to a cron job.
67 # NOTE: All other tasks are acomplished via crons (or the parent dare)
68 pledge_id = pledge.doc_id
69 main_result = AppResult[PledgeWrapper](
70 doc_id=pledge_id,
71 task_name=f"trigger pledge for {pledge_id}",
72 )
74 result = self._update_pledge_stats(pledge)
75 if not result.is_error:
76 msg = f"Pledge {pledge_id} stats updated successfully."
77 if IS_TRACE:
78 LOG().trace(msg)
80 return JobResult.ok(doc_id=pledge_id, message=msg)
82 # handle error
83 if IS_TRACE:
84 LOG().trace(f"Pledge {pledge_id} stats update failed with error: {result.error_str}")
86 main_result.merge(result)
87 return JobResult.from_result(main_result, data=job.to_json_dict())
89 def _update_pledge_stats(self, pledge: PledgeWrapper) -> AppResult[DareWrapper]:
90 # we update the stats for the corresponding DareWrapper
91 pledge_id = pledge.doc_id
92 main_result = AppResult[DareWrapper](doc_id=pledge_id)
94 dare_db = self.dare_db
95 dare_id = pledge.dare_id
96 dare_model = dare_db.get(dare_id)
97 if dare_model is None:
98 msg = (
99 f"Dare model not found for Dare ID {dare_id}/ "
100 f"Pledge ID {pledge_id} during pledge stats update."
101 )
102 self.job_error(
103 error_code=AppErrorCode.NOT_FOUND,
104 message=msg,
105 doc_id=dare_id,
106 job_type=AppJobType.TR_PLEDGE,
107 )
108 main_result.add_error(AppErrorCode.NOT_FOUND, msg)
109 return main_result
111 pledge_amount = pledge.amount
112 pledge_currency = pledge.currency_code
113 pledge_stats = dare_model.pledge_stats
114 pledge_stats.pending.count += 1
115 usd_cents = self.exchange_rate_monitor.convert_cents_to_usd_cents(
116 pledge_amount, pledge_currency
117 )
118 if usd_cents is None:
119 msg = f"Failed to convert pledge amount to USD cents for pledge ID {pledge_id}."
120 LOG().warning(msg)
121 self.job_error(
122 error_code=AppErrorCode.CURRENCY_CONVERSION,
123 message=msg,
124 doc_id=pledge_id,
125 job_type=AppJobType.TR_PLEDGE,
126 )
127 usd_cents = pledge_amount # Fallback to original amount in case of conversion failure
129 pledge_stats.pending.cents += pledge_amount
130 pledge_stats.pending.cents_usd += usd_cents
132 dare_model.pledge_stats = pledge_stats
133 updates = dare_model.get_updates()
134 if not updates:
135 msg = f"No updates found for Dare with ID {dare_id} during pledge stats update."
136 LOG().warning(msg)
137 self.job_error(
138 error_code=AppErrorCode.UNEXPECTED_CODE_PATH,
139 message=msg,
140 doc_id=dare_id,
141 job_type=AppJobType.TR_PLEDGE,
142 )
143 main_result.generated = dare_model
144 return main_result
146 if IS_DEBUG:
147 LOG().debug(f"Updating Dare {dare_id} stats: {updates}")
149 updated_dare = dare_db.update(dare_id, updates)
150 if updated_dare is not None:
151 if IS_DEBUG:
152 msg = f"Successfully updated Dare with ID {dare_id} during pledge stats update."
153 LOG().debug(msg)
155 main_result.generated = dare_model
156 return main_result
158 msg = f"Failed to update Dare with ID {dare_id} during pledge stats update."
159 LOG().error(msg)
160 self.job_error(
161 error_code=AppErrorCode.UPDATE_FAILED,
162 message=msg,
163 doc_id=dare_id,
164 job_type=AppJobType.TR_PLEDGE,
165 )
166 main_result.add_error(AppErrorCode.UPDATE_FAILED, msg)
167 return main_result