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

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 

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 

29 

30__all__ = ["PledgeService"] 

31 

32 

33_JT = AppJobType 

34_COL = FirestoreCollections.PLEDGE 

35 

36 

37class PledgeService(ErrorMixin, ServiceProvider): 

38 

39 def __init__( 

40 self, 

41 client: AppPaymentService | None = None, 

42 ) -> None: 

43 super().__init__() 

44 

45 self._client = client 

46 

47 @property 

48 def client(self) -> AppPaymentService: 

49 if self._client is None: 

50 self._client = AppPaymentService() 

51 return self._client 

52 

53 # ======================================================================== 

54 # TRIGGERS - Delegate to processors 

55 # ======================================================================== 

56 

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 ) 

73 

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) 

79 

80 return JobResult.ok(doc_id=pledge_id, message=msg) 

81 

82 # handle error 

83 if IS_TRACE: 

84 LOG().trace(f"Pledge {pledge_id} stats update failed with error: {result.error_str}") 

85 

86 main_result.merge(result) 

87 return JobResult.from_result(main_result, data=job.to_json_dict()) 

88 

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) 

93 

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 

110 

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 

128 

129 pledge_stats.pending.cents += pledge_amount 

130 pledge_stats.pending.cents_usd += usd_cents 

131 

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 

145 

146 if IS_DEBUG: 

147 LOG().debug(f"Updating Dare {dare_id} stats: {updates}") 

148 

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) 

154 

155 main_result.generated = dare_model 

156 return main_result 

157 

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