Coverage for functions \ flipdare \ payments \ data \ fee_calculator.py: 98%
83 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#
14import math
16from flipdare.app_log import LOG
17from flipdare.constants import STRIPE_BASE_FEES, IS_DEBUG, STRIPE_FEE_MAPPING
18from flipdare.generated.shared.model.user.app_fee_type import AppFeeType
19from flipdare.generated.shared.stripe.stripe_currency_code import StripeCurrencyCode
20from flipdare.payments.payment_types import StripeRegionFee
23class FeeCalculator:
24 __slots__ = ("_amount", "_fee_level", "_from_currency", "_to_currency")
26 def __init__(
27 self,
28 amount: int,
29 fee_type: AppFeeType,
30 from_currency: StripeCurrencyCode,
31 to_currency: StripeCurrencyCode,
32 ) -> None:
33 self._amount = amount
34 self._fee_level = fee_type
35 self._from_currency = from_currency
36 self._to_currency = to_currency
38 @staticmethod
39 def currency_fees(currency_code: StripeCurrencyCode) -> StripeRegionFee:
40 return STRIPE_BASE_FEES[STRIPE_FEE_MAPPING.get(currency_code, "US")]
42 @property
43 def amount(self) -> int:
44 return self._amount
46 @property
47 def from_currency(self) -> StripeCurrencyCode:
48 return self._from_currency
50 @property
51 def to_currency(self) -> StripeCurrencyCode:
52 return self._to_currency
54 @property
55 def flipdare_percent_fee(self) -> float:
56 return self._fee_level.percent / 100.0
58 @property
59 def app_fee_amount(self) -> int:
60 percent_fee = self.flipdare_percent_fee
62 fee_amount = self._amount * percent_fee
63 if IS_DEBUG:
64 msg = f"Calculating App Fee: {self._amount} * {percent_fee:.2%} = {fee_amount}"
65 LOG().debug(msg)
67 # round up to nearest cent to ensure we never under-charge
68 return math.ceil(fee_amount)
70 @property
71 def is_cross_transfer(self) -> bool:
72 return self._to_currency != self._from_currency
74 @property
75 def is_cross_region(self) -> bool:
76 from_currency = self._from_currency
77 to_currency = self._to_currency
79 from_region = self.currency_fees(from_currency)
80 to_region = self.currency_fees(to_currency)
81 return from_region != to_region
83 @property
84 def conservative_app_fee_amount(self) -> int:
85 return self.app_fee_amount + self.conservative_stripe_fee
87 @property
88 def conservative_net_amount(self) -> int:
89 return self._amount - self.conservative_app_fee_amount
91 @property
92 def conservative_stripe_fee(self) -> int:
93 # this is an estimate of the max fee available,
94 # we can then refund any excess after the fact if we over-charge.
95 from_currency = self._from_currency
96 to_currency = self._to_currency
97 amount = self._amount
99 region = self.currency_fees(from_currency)
100 total_rate = region.base_rate + region.intl_surcharge + region.currency_surcharge
101 gross_amount = amount / (1 - total_rate)
102 gross_fee = math.ceil(gross_amount - amount)
104 if IS_DEBUG:
105 LOG().debug(
106 f"Conservative Calculation for {amount} {from_currency} -> {to_currency}\n"
107 f"\tTotal Rate: {total_rate:.2f}%\n"
108 f"\tTotal Effective Rate: {total_rate:.2f}%\n"
109 f"\tEstimated Fee: {gross_fee} %\n",
110 )
112 return gross_fee
114 def calculate_fee(self) -> int:
115 from_currency = self._from_currency
116 to_currency = self._to_currency
118 # this is stored and adjusted in the database,
119 # so we can be more precise in our calculations here to minimize the risk of over-charging and needing to refund later.
120 region = self.currency_fees(from_currency)
122 total_rate = region.base_rate
123 currency_surcharge = 0.0
124 intl_surcharge = 0.0
126 if from_currency != to_currency:
127 currency_surcharge = region.currency_surcharge
129 total_rate += currency_surcharge
131 # Determine if international (different regions)
132 # Note: In reality, this usually depends on the CARD country, not settlement currency
133 if self.currency_fees(from_currency) != region:
134 intl_surcharge = region.intl_surcharge
136 total_rate += intl_surcharge
138 # Correct Formula for Total Gross to cover fees:
139 # Gross = (Net + Fixed) / (1 - Rate)
140 gross_amount = (self._amount + region.fixed_fee) / (1 - total_rate)
142 # Round up (ceil) to the nearest cent to ensure you never under-charge
143 final_gross = math.ceil(gross_amount)
145 # Stripe fee is the gross amount minus the net amount (what you want to receive)
146 stripe_fee = round(final_gross - self._amount)
148 if IS_DEBUG:
149 LOG().debug(
150 f"Fee Calculation for {self._amount} {from_currency} -> {self._to_currency}\n"
151 f"\tTotal Rate: {total_rate:.2f}%\n"
152 f'\tInternational Surcharge: {f"{intl_surcharge:.2f}%" if self.is_cross_region else "(n/a)"}\n'
153 f'\tCurrency Surcharge: {f"{currency_surcharge:.2f}%" if self.is_cross_transfer else "(n/a)"}\n'
154 f"\tTotal Effective Rate: {total_rate:.2f}%\n"
155 f"\tApp Fee: {self.app_fee_amount}\n"
156 f"\tConservative Stripe Fee: {self.conservative_stripe_fee}\n"
157 f"\tConservative Net Amount: {self.conservative_net_amount}\n"
158 f"\tEstimated Fee: {stripe_fee} %\n",
159 )
161 return stripe_fee