Coverage for functions \ flipdare \ payments \ data \ payment_schedule.py: 88%
126 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#
12from flipdare.app_log import LOG
13from flipdare.constants import IS_DEBUG
14from flipdare.generated.model.payment.payment_model import PaymentModel
15from flipdare.generated.model.payment.payment_schedule_model import PaymentScheduleModel
16from flipdare.generated.shared.payment.payment_status import PaymentStatus
17from flipdare.payments.data.payment_event_context import PaymentEventContext
18from flipdare.util.time_util import TimeUtil
19from flipdare.wrapper.payment.payment_event_wrapper import PaymentEventWrapper
22class PaymentSchedule:
24 __slots__ = (
25 "_can_schedule",
26 "_events",
27 "_evt_ctx",
28 "_has_changed",
29 "_payment",
30 "_pledge_id",
31 "_updated_schedule",
32 "_updated_status",
33 )
35 _payment: PaymentModel
36 _pledge_id: str
38 _events: list[PaymentEventWrapper]
39 _evt_ctx: PaymentEventContext
40 _can_schedule: bool
41 _has_changed: bool
42 _updated_schedule: PaymentScheduleModel | None
43 _updated_status: PaymentStatus | None
45 def __init__(
46 self, pledge_id: str, payment: PaymentModel, events: list[PaymentEventWrapper]
47 ) -> None:
48 self._pledge_id = pledge_id
49 self._payment = payment
50 self._events = events
51 self._evt_ctx = PaymentEventContext(amount=payment.amount, events=events)
52 self._can_schedule = False
53 self._has_changed = False
54 self._updated_schedule = None
55 self._updated_status = None
56 self._update_schedule()
58 @property
59 def can_schedule(self) -> bool:
60 return self._can_schedule
62 @property
63 def updated_schedule(self) -> PaymentScheduleModel | None:
64 return self._updated_schedule
66 @property
67 def updated_status(self) -> PaymentStatus | None:
68 return self._updated_status
70 @property
71 def _dbg_msg(self) -> str:
72 payment = self._payment
73 doc_id = self._pledge_id
74 payment_method_id = payment.payment_method_id
75 return f"(pledge {doc_id}, pi_id={payment.payment_intent_id}, pm_id={payment_method_id})"
77 def _update_schedule(self) -> PaymentScheduleModel | None: # noqa: PLR0915, PLR0912
78 can_schedule = self._payment.last_event is not None
80 # we can only schedule if there is a payment event with an amount_captured > 0
81 if not can_schedule:
82 if IS_DEBUG:
83 msg = f"{self._dbg_msg} - cannot schedule payment because there are no payment events with a captured amount yet."
84 LOG().debug(msg)
85 self._can_schedule = False
86 return None
88 cur_schedule = self._payment.schedule
89 # we set the dates based on the available_on date.
90 available_on = cur_schedule.available_on
91 if available_on is None:
92 # this means the charge has not been captured.
93 if IS_DEBUG:
94 msg = f"{self._dbg_msg} - cannot schedule payment because there is no captured charge yet."
95 LOG().debug(msg)
97 self._can_schedule = False
98 return None
100 payment_status = self._payment.status
102 # check existing status
103 if payment_status == PaymentStatus.CAPTURE and self.is_status_complete(
104 PaymentStatus.CAPTURE
105 ):
106 if IS_DEBUG:
107 msg = f"{self._dbg_msg} - Payment is already in CAPTURE status and capture is complete. No need to update schedule."
108 LOG().debug(msg)
110 self._can_schedule = False
111 return None
112 elif payment_status == PaymentStatus.REFUND and self.is_status_complete(
113 PaymentStatus.REFUND
114 ):
115 if IS_DEBUG:
116 msg = f"{self._dbg_msg} - Payment is already in REFUND status and refund is complete. No need to update schedule."
117 LOG().debug(msg)
119 self._can_schedule = False
120 return None
121 elif payment_status == PaymentStatus.TRANSFER and self.is_status_complete(
122 PaymentStatus.TRANSFER
123 ):
124 if IS_DEBUG:
125 msg = f"{self._dbg_msg} - Payment is already in TRANSFER status and transfer is complete. No need to update schedule."
126 LOG().debug(msg)
128 self._can_schedule = False
129 return None
131 # if we get here the status is:
132 # PaymentStatus.WAITING, PaymentStatus.PENDING, PaymentStatus.HOLD
133 # or is incomplete.
134 available_on_dt = TimeUtil.epoch_to_utc_dt(available_on)
135 delay_days = self._payment.risk_assessment.delay_days
136 transfer_on_dt = TimeUtil.get_utc_time_days_ago_from(available_on_dt, delay_days)
138 schedule = PaymentScheduleModel(
139 available_on=available_on, transfer_on=TimeUtil.dt_to_epoch(transfer_on_dt)
140 )
141 updated_status: PaymentStatus | None = None
142 if IS_DEBUG:
143 msg = f"{self._dbg_msg} - current payment status: {payment_status}, available_on: {available_on_dt}, delay_days: {delay_days}"
144 LOG().debug(msg)
146 if payment_status == PaymentStatus.REFUND:
147 # set the refund date to earliest possible date (available_on) to ensure refunds are processed asap
148 if IS_DEBUG:
149 msg = f"{self._dbg_msg} - Payment is in REFUND status. Setting refund_on to {available_on_dt}."
150 LOG().debug(msg)
152 schedule.transfer_on = None
153 schedule.refund_on = available_on
154 updated_status = PaymentStatus.REFUND
155 # we need to check if we can transfer or we need to capture first..
156 #
157 elif self.is_status_complete(PaymentStatus.CAPTURE):
158 # if capture is complete, we can transfer immediately.
159 if IS_DEBUG:
160 msg = f"{self._dbg_msg} - capture is complete. Setting transfer_on to {transfer_on_dt}."
161 LOG().debug(msg)
163 updated_status = PaymentStatus.TRANSFER
164 else:
165 # if capture is not complete, we set the capture date to the available_on date to ensure it is captured asap.
166 if IS_DEBUG:
167 msg = f"{self._dbg_msg} - capture is not complete. Setting capture_on to {available_on_dt}."
168 LOG().debug(msg)
170 schedule.capture_on = available_on
171 updated_status = PaymentStatus.CAPTURE
173 self._can_schedule = True
174 self._has_changed = True
175 self._updated_schedule = schedule
176 self._updated_status = updated_status
177 return schedule
179 def is_status_complete(self, status: PaymentStatus) -> bool:
180 evt_ctx = self._evt_ctx
182 # captured on is the most important date- it determines all the other dates...
183 captured_on = evt_ctx.captured_on
184 if captured_on is None:
185 return False
187 match status:
188 case PaymentStatus.WAITING | PaymentStatus.PENDING | PaymentStatus.HOLD:
189 return False
190 case PaymentStatus.CAPTURE:
191 return evt_ctx.capture_complete
192 case PaymentStatus.TRANSFER:
193 return evt_ctx.transfer_complete
194 case PaymentStatus.REFUND:
195 return evt_ctx.refund_complete
196 case PaymentStatus.COMPLETE:
197 return (
198 evt_ctx.capture_complete
199 or evt_ctx.transfer_complete
200 or evt_ctx.refund_complete
201 )
202 case PaymentStatus.FAILED:
203 # if it's failed, it's complete in the sense that we won't process it anymore.
204 return True