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
« 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 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
29__all__ = ["FriendService"]
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
38class FriendService(ServiceProvider):
39 """
40 Coordinator for friend and invite operations.
41 Uses composition to delegate to specialized processors.
42 """
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
56 @property
57 def invite_processor(self) -> InviteProcessor:
58 from flipdare.service.processor.invite_processor import InviteProcessor
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
71 @property
72 def friend_processor(self) -> FriendProcessor:
73 from flipdare.service.processor.friend_processor import FriendProcessor
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
84 # ========================================================================
85 # CRONS
86 # ========================================================================
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
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()
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
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()
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()
129 # ========================================================================
130 # TRIGGERS - Delegate to processors
131 # ========================================================================
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)
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)
166 return self.invite_processor.process_new_invite(wrapper)
168 # # ========================================================================
169 # # PUBLIC API - Delegate to processors
170 # # ========================================================================
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