Coverage for functions \ flipdare \ service \ friend_service.py: 67%

69 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 functools import partial 

15from typing import TYPE_CHECKING 

16from flipdare.result.outcome import Outcome 

17from flipdare.result.job_result import JobResult 

18from flipdare.core.cron_decorator import cron_decorator 

19from flipdare.core.trigger_decorator import trigger_decorator 

20from flipdare.service._service_provider import ServiceProvider 

21from flipdare.app_log import LOG 

22from flipdare.app_types import CronResult 

23from flipdare.core.job_type_decorator import job_type_decorator 

24from flipdare.generated.shared.backend.app_job_type import AppJobType 

25from flipdare.generated.shared.firestore_collections import FirestoreCollections 

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

27from flipdare.wrapper import AppJobWrapper, FriendWrapper, InviteWrapper 

28 

29__all__ = ["FriendService"] 

30 

31if TYPE_CHECKING: 

32 from flipdare.service.processor.friend_processor import FriendProcessor 

33 from flipdare.service.processor.invite_processor import InviteProcessor 

34 from flipdare.manager.db_manager import DbManager 

35 from flipdare.manager.backend_manager import BackendManager 

36 

37 

38class FriendService(ServiceProvider): 

39 """ 

40 Coordinator for friend and invite operations. 

41 Uses composition to delegate to specialized processors. 

42 """ 

43 

44 def __init__( 

45 self, 

46 db_manager: DbManager | None = None, 

47 backend_manager: BackendManager | None = None, 

48 ) -> None: 

49 super().__init__( 

50 backend_manager=backend_manager, 

51 db_manager=db_manager, 

52 ) 

53 self._invite_processor: InviteProcessor | None = None 

54 self._friend_processor: FriendProcessor | None = None 

55 

56 @property 

57 def invite_processor(self) -> InviteProcessor: 

58 from flipdare.service.processor.invite_processor import InviteProcessor 

59 

60 if self._invite_processor is None: 

61 # Initialize processors with their dependencies 

62 self._invite_processor = InviteProcessor( 

63 indexer_service=self.indexer, 

64 invite_bridge=self.invite_bridge, 

65 user_bridge=self.user_bridge, 

66 friend_bridge=self.friend_bridge, 

67 mailer=self.user_mailer, 

68 ) 

69 return self._invite_processor 

70 

71 @property 

72 def friend_processor(self) -> FriendProcessor: 

73 from flipdare.service.processor.friend_processor import FriendProcessor 

74 

75 if self._friend_processor is None: 

76 self._friend_processor = FriendProcessor( 

77 friend_bridge=self.friend_bridge, 

78 notification_service=self.notification_service, 

79 indexer_service=self.indexer, 

80 summary_service=self.service_manager.summary, 

81 ) 

82 return self._friend_processor 

83 

84 # ======================================================================== 

85 # CRONS 

86 # ======================================================================== 

87 

88 @cron_decorator(job_type=AppJobType.CR_INVITE_REMINDER) 

89 def cron_invite_reminder(self) -> CronResult: 

90 """Send invite reminders for invites older than 7 days.""" 

91 invite_processor = self.invite_processor 

92 job_type = AppJobType.CR_INVITE_REMINDER 

93 

94 config = CronConfig( 

95 job_type=job_type, 

96 job_name=job_type.value, 

97 query_fn=partial(self.invite_db.get_reminder_invites), 

98 process_fn=lambda invite: invite_processor.process_invite_reminder(invite), 

99 ) 

100 return CronProcessor(config).process_result() 

101 

102 @cron_decorator(job_type=AppJobType.CR_INVITE_UNPROCESSED) 

103 def cron_invite_unprocessed(self) -> CronResult: 

104 """Process unprocessed invites from last week.""" 

105 invite_processor = self.invite_processor 

106 job_type = AppJobType.CR_INVITE_UNPROCESSED 

107 

108 config = CronConfig( 

109 job_type=job_type, 

110 job_name=job_type.value, 

111 query_fn=partial(self.invite_db.get_recent_unprocessed_invites), 

112 process_fn=lambda invite: invite_processor.process_invite_signup(invite), 

113 ) 

114 return CronProcessor(config).process_result() 

115 

116 @cron_decorator(job_type=AppJobType.CR_FRIEND_UNPROCESSED) 

117 def cron_friend_unprocessed(self) -> CronResult: 

118 """Process unprocessed friends from last week.""" 

119 friend_processor = self.friend_processor 

120 job_type = AppJobType.CR_FRIEND_UNPROCESSED 

121 config = CronConfig( 

122 job_type=job_type, 

123 job_name=job_type.value, 

124 query_fn=partial(self.friend_db.get_unprocessed_friends_last_week), 

125 process_fn=lambda friend: friend_processor.process_friend_update(friend), 

126 ) 

127 return CronProcessor(config).process_result() 

128 

129 # ======================================================================== 

130 # TRIGGERS - Delegate to processors 

131 # ======================================================================== 

132 

133 @job_type_decorator(AppJobType.TR_FRIEND) 

134 @trigger_decorator( 

135 job_type=AppJobType.TR_FRIEND, 

136 collection=FirestoreCollections.FRIEND, 

137 wrapper_class=FriendWrapper, 

138 ) 

139 def trigger_friend( 

140 self, 

141 job: AppJobWrapper, 

142 *, 

143 wrapper: FriendWrapper, 

144 ) -> JobResult[FriendWrapper]: 

145 """Trigger for friend create/update events.""" 

146 processor = self.friend_processor 

147 if not job.has_changes: 

148 return processor.process_new_friend(wrapper) 

149 else: 

150 return processor.process_friend_update(wrapper) 

151 

152 @job_type_decorator(AppJobType.TR_INVITE) 

153 @trigger_decorator(job_type=AppJobType.TR_INVITE, collection=FirestoreCollections.INVITE) 

154 def trigger_invite( 

155 self, 

156 job: AppJobWrapper, 

157 *, 

158 wrapper: InviteWrapper, 

159 ) -> JobResult[InviteWrapper]: 

160 """Trigger for invite create/update events.""" 

161 if not job.has_changes and wrapper.processed: 

162 msg = f"Invite {wrapper.doc_id} already processed on create trigger, skipping." 

163 LOG().info(msg) 

164 return JobResult.skip_doc(doc_id=wrapper.doc_id, message=msg) 

165 

166 return self.invite_processor.process_new_invite(wrapper) 

167 

168 # # ======================================================================== 

169 # # PUBLIC API - Delegate to processors 

170 # # ======================================================================== 

171 

172 def trigger_invite_signup(self, invite_model: InviteWrapper) -> Outcome: 

173 """Process when invited user signs up.""" 

174 result = self.invite_processor.process_invite_signup(invite_model) 

175 return result.outcome