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

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 

13 

14import math 

15 

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 

21 

22 

23class FeeCalculator: 

24 __slots__ = ("_amount", "_fee_level", "_from_currency", "_to_currency") 

25 

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 

37 

38 @staticmethod 

39 def currency_fees(currency_code: StripeCurrencyCode) -> StripeRegionFee: 

40 return STRIPE_BASE_FEES[STRIPE_FEE_MAPPING.get(currency_code, "US")] 

41 

42 @property 

43 def amount(self) -> int: 

44 return self._amount 

45 

46 @property 

47 def from_currency(self) -> StripeCurrencyCode: 

48 return self._from_currency 

49 

50 @property 

51 def to_currency(self) -> StripeCurrencyCode: 

52 return self._to_currency 

53 

54 @property 

55 def flipdare_percent_fee(self) -> float: 

56 return self._fee_level.percent / 100.0 

57 

58 @property 

59 def app_fee_amount(self) -> int: 

60 percent_fee = self.flipdare_percent_fee 

61 

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) 

66 

67 # round up to nearest cent to ensure we never under-charge 

68 return math.ceil(fee_amount) 

69 

70 @property 

71 def is_cross_transfer(self) -> bool: 

72 return self._to_currency != self._from_currency 

73 

74 @property 

75 def is_cross_region(self) -> bool: 

76 from_currency = self._from_currency 

77 to_currency = self._to_currency 

78 

79 from_region = self.currency_fees(from_currency) 

80 to_region = self.currency_fees(to_currency) 

81 return from_region != to_region 

82 

83 @property 

84 def conservative_app_fee_amount(self) -> int: 

85 return self.app_fee_amount + self.conservative_stripe_fee 

86 

87 @property 

88 def conservative_net_amount(self) -> int: 

89 return self._amount - self.conservative_app_fee_amount 

90 

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 

98 

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) 

103 

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 ) 

111 

112 return gross_fee 

113 

114 def calculate_fee(self) -> int: 

115 from_currency = self._from_currency 

116 to_currency = self._to_currency 

117 

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) 

121 

122 total_rate = region.base_rate 

123 currency_surcharge = 0.0 

124 intl_surcharge = 0.0 

125 

126 if from_currency != to_currency: 

127 currency_surcharge = region.currency_surcharge 

128 

129 total_rate += currency_surcharge 

130 

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 

135 

136 total_rate += intl_surcharge 

137 

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) 

141 

142 # Round up (ceil) to the nearest cent to ensure you never under-charge 

143 final_gross = math.ceil(gross_amount) 

144 

145 # Stripe fee is the gross amount minus the net amount (what you want to receive) 

146 stripe_fee = round(final_gross - self._amount) 

147 

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 ) 

160 

161 return stripe_fee