Coverage for functions \ flipdare \ service \ user_stats_service.py: 0%
65 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 typing import TYPE_CHECKING
16from flipdare.service._service_provider import ServiceProvider
17from flipdare.app_log import LOG
18from flipdare.constants import IS_DEBUG
19from flipdare.result.app_result import AppResult
20from flipdare.generated import AppJobType
21from flipdare.generated.shared.app_error_code import AppErrorCode
22from flipdare.generated.shared.firestore_collections import FirestoreCollections
23from flipdare.wrapper.dare_wrapper import DareWrapper
24from flipdare.wrapper.payment.pledge_wrapper import PledgeWrapper
26if TYPE_CHECKING:
27 from flipdare.manager.db_manager import DbManager
28 from flipdare.manager.backend_manager import BackendManager
30__all__ = ["UserStatsService"]
33class UserStatsService(ServiceProvider):
34 def __init__(
35 self,
36 db_manager: DbManager | None = None,
37 backend_manager: BackendManager | None = None,
38 ) -> None:
39 super().__init__(
40 backend_manager=backend_manager,
41 db_manager=db_manager,
42 )
44 # TODO: UserStatsService - We separate stats (views, likes, dislikes) etc using a custom callback
45 """
46 Firestore has a limitation of 1 write per document per second.
47 So in the flutter app:
48 1. direct updates e.g. dare updates, user profile etc
49 - these updates are done directly to the user document in firestore
50 - since the likelihood of multiple updates.
51 - for things like chats, this is also ok since we can just append to a subcollection and not update the same document
52 2. stats updates (views, likes, dislikes etc)
53 - the user makes a direct call to a callback endpoint.
54 - we add a job if there is enough updates to trigger
55 - since we are adding a document, we can have multiple updates per second without hitting the 1 write per document per second limit
56 - the job will then process the updates and update the user document in firestore
58 this should be a good balance between consistency and performance.
59 if there are performance issue STATS_SENSITIVITY may need to be tweaked.
61 FIXME: prioritizing updates so we can process the low priority updates offpeak.
63 """
65 # ========================================================================
66 # Helpers
67 # ========================================================================
69 def _update_pledge_stats(self, pledge: PledgeWrapper) -> AppResult[DareWrapper]:
70 """Update pledge statistics after a pledge is created or updated."""
71 pledge_id = pledge.doc_id
72 main_result = AppResult[DareWrapper](doc_id=pledge_id)
74 log_creator = self.app_logger
75 col = FirestoreCollections.PLEDGE
76 job_type = AppJobType.TR_PLEDGE
78 dare_db = self.dare_db
79 dare_id = pledge.dare_id
80 dare_model = dare_db.get(dare_id)
81 if dare_model is None:
82 msg = (
83 f"Dare model not found for Dare ID {dare_id}/ "
84 f"Pledge ID {pledge_id} during pledge stats update."
85 )
86 LOG().error(msg)
87 log_creator.system_error(
88 error_code=AppErrorCode.NOT_FOUND,
89 collection=col,
90 message=msg,
91 doc_id=dare_id,
92 job_type=job_type,
93 )
94 main_result.add_error(AppErrorCode.NOT_FOUND, msg)
95 return main_result
97 pledge_amount = pledge.amount
98 pledge_currency = pledge.currency_code
99 pledge_stats = dare_model.pledge_stats
100 pledge_stats.pending.count += 1
101 usd_cents = self.exchange_rate_monitor.convert_cents_to_usd_cents(
102 pledge_amount, pledge_currency
103 )
104 if usd_cents is None:
105 msg = f"Failed to convert pledge amount to USD cents for pledge ID {pledge_id}."
106 LOG().warning(msg)
107 log_creator.job_error(
108 error_code=AppErrorCode.CURRENCY_CONVERSION,
109 collection=col,
110 message=msg,
111 doc_id=pledge_id,
112 job_type=job_type,
113 )
114 usd_cents = pledge_amount # Fallback to original amount in case of conversion failure
116 pledge_stats.pending.cents += pledge_amount
117 pledge_stats.pending.cents_usd += usd_cents
119 dare_model.pledge_stats = pledge_stats
120 updates = dare_model.get_updates()
121 if not updates:
122 msg = f"No updates found for Dare with ID {dare_id} during pledge stats update."
123 LOG().warning(msg)
124 log_creator.job_error(
125 error_code=AppErrorCode.UNEXPECTED_CODE_PATH,
126 collection=col,
127 message=msg,
128 doc_id=dare_id,
129 job_type=job_type,
130 )
131 main_result.generated = dare_model
132 return main_result
134 if IS_DEBUG:
135 LOG().debug(f"Updating Dare {dare_id} stats: {updates}")
137 updated_dare = dare_db.update(dare_id, updates)
138 if updated_dare is not None:
139 if IS_DEBUG:
140 msg = f"Successfully updated Dare with ID {dare_id} during pledge stats update."
141 LOG().debug(msg)
143 main_result.generated = dare_model
144 return main_result
146 msg = f"Failed to update Dare with ID {dare_id} during pledge stats update."
147 LOG().error(msg)
148 log_creator.system_error(
149 error_code=AppErrorCode.UPDATE_FAILED,
150 collection=col,
151 message=msg,
152 doc_id=dare_id,
153 job_type=job_type,
154 )
155 main_result.add_error(AppErrorCode.UPDATE_FAILED, msg)
156 return main_result