33"""
44import argparse
55from collections import namedtuple
6+ import decimal
67from decimal import Decimal
78import sys
8- from typing import Any , Dict
9+ from typing import Any , Dict , Optional
10+ from attr import dataclass
911import urllib3
1012
1113from rich .pretty import pprint
1618
1719urllib3 .disable_warnings (urllib3 .exceptions .InsecureRequestWarning )
1820
19- AdjustedCoins : namedtuple = namedtuple ("AdjustedCoins" ,
20- ["coins" , "fc" , "ac" , "aac" ])
21+ @dataclass
22+ class AdjustedCoins :
23+ coins : Decimal
24+ fc : Decimal
25+ ac : Decimal
26+ aac : Decimal
2127
2228
2329def main (c_args : argparse .Namespace ) -> None :
@@ -36,6 +42,9 @@ def main(c_args: argparse.Namespace) -> None:
3642 username = env .admin_user ,
3743 password = env .admin_password ,
3844 )
45+ if not token :
46+ console .log ("[bold red]ERROR[/bold red] Failed to get token" )
47+ sys .exit (1 )
3948
4049 # Get the product details.
4150 # This gives us the product's allowance, limit and overspend multipliers
@@ -55,6 +64,11 @@ def main(c_args: argparse.Namespace) -> None:
5564
5665 remaining_days : int = p_rv .msg ["product" ]["coins" ]["remaining_days" ]
5766
67+ # What's the 'billing prediction' in the /product response?
68+ # We'll compare this later to ensure it matches what we find
69+ # when we calculate the cost to the user using the product charges.
70+ product_response_billing_prediction : Decimal = round (Decimal (p_rv .msg ["product" ]["coins" ]["billing_prediction" ]), 2 )
71+
5872 # Get the product's charges...
5973 pc_rv : AsApiRv = AsApi .get_product_charges (token , product_id = args .product , pbp = args .pbp )
6074 if not pc_rv .success :
@@ -65,16 +79,16 @@ def main(c_args: argparse.Namespace) -> None:
6579 pprint (pc_rv .msg )
6680
6781 # Accumulate all the storage costs
68- # (excluding the current which will be interpreted as the "burn rate")
82+ # (the current record wil be used to set the future the "burn rate")
6983 num_storage_charges : int = 0
7084 burn_rate : Decimal = Decimal ()
7185 total_storage_coins : Decimal = Decimal ()
7286 if "items" in pc_rv .msg ["storage_charges" ]:
7387 for item in pc_rv .msg ["storage_charges" ]["items" ]:
88+ total_storage_coins += Decimal (item ["coins" ])
7489 if "current_bytes" in item ["additional_data" ]:
75- burn_rate = Decimal (item ["coins " ])
90+ burn_rate = Decimal (item ["burn_rate " ])
7691 else :
77- total_storage_coins += Decimal (item ["coins" ])
7892 num_storage_charges += 1
7993
8094 # Accumulate all the processing costs
@@ -128,31 +142,58 @@ def main(c_args: argparse.Namespace) -> None:
128142 "Coins (Adjusted)" : f"{ ac .fc } + { ac .aac } = { ac .coins } " ,
129143 }
130144
131- additional_coins : Decimal = total_uncommitted_processing_coins + burn_rate * remaining_days
145+ # We've accumulated today's storage costs (based on the current 'peak'),
146+ # so we can only predict further storage costs if there's more than
147+ # 1 day left until the billing day. And that 'burn rate' is based on today's
148+ # 'current' storage, not its 'peak'.
149+ burn_rate_contribution : Decimal = Decimal ()
150+ burn_rate_days : int = max (remaining_days - 1 , 0 )
151+ if burn_rate_days > 0 :
152+ burn_rate_contribution = burn_rate * burn_rate_days
153+ additional_coins : Decimal = total_uncommitted_processing_coins + burn_rate_contribution
132154 predicted_total_coins : Decimal = total_coins
133155 zero : Decimal = Decimal ()
134- if remaining_days > 0 :
135- if burn_rate > zero :
136-
137- predicted_total_coins += additional_coins
138- p_ac : AdjustedCoins = _calculate_adjusted_coins (
139- predicted_total_coins ,
140- allowance ,
141- allowance_multiplier )
142-
143- invoice ["Prediction" ] = {
144- "Coins (Burn Rate)" : str (burn_rate ),
145- "Coins (Additional Spend)" : f"{ total_uncommitted_processing_coins } + { remaining_days } x { burn_rate } = { additional_coins } " ,
146- "Coins (Total Raw)" : f"{ total_coins } + { additional_coins } = { predicted_total_coins } " ,
147- "Coins (Penalty Free)" : str (p_ac .fc ),
148- "Coins (In Allowance Band)" : str (p_ac .ac ),
149- "Coins (Allowance Charge)" : f"{ p_ac .ac } x { allowance_multiplier } = { p_ac .aac } " ,
150- "Coins (Adjusted)" : f"{ p_ac .fc } + { p_ac .aac } = { p_ac .coins } " ,
151- }
156+ calculated_billing_prediction : Decimal = Decimal ()
157+
158+ if remaining_days > 0 and burn_rate > zero :
159+
160+ predicted_total_coins += additional_coins
161+ p_ac : AdjustedCoins = _calculate_adjusted_coins (
162+ predicted_total_coins ,
163+ allowance ,
164+ allowance_multiplier )
165+
166+ invoice ["Prediction" ] = {
167+ "Coins (Burn Rate)" : str (burn_rate ),
168+ "Coins (Expected Burn Rate Contribution)" : f"{ burn_rate_days } x { burn_rate } = { burn_rate_contribution } " ,
169+ "Coins (Additional Spend)" : f"{ total_uncommitted_processing_coins } + { burn_rate_contribution } = { additional_coins } " ,
170+ "Coins (Total Raw)" : f"{ total_coins } + { additional_coins } = { predicted_total_coins } " ,
171+ "Coins (Penalty Free)" : str (p_ac .fc ),
172+ "Coins (In Allowance Band)" : str (p_ac .ac ),
173+ "Coins (Allowance Charge)" : f"{ p_ac .ac } x { allowance_multiplier } = { p_ac .aac } " ,
174+ "Coins (Adjusted)" : f"{ p_ac .fc } + { p_ac .aac } = { p_ac .coins } " ,
175+ }
176+
177+ calculated_billing_prediction = p_ac .coins
152178
153179 # Now just pre-tty-print the invoice
154180 pprint (invoice )
155181
182+ console .log (f"Calculated billing prediction is { calculated_billing_prediction } " )
183+ console .log (f"Product response billing prediction is { product_response_billing_prediction } " )
184+
185+ if calculated_billing_prediction == product_response_billing_prediction :
186+ console .log (":white_check_mark: CORRECT - Predictions match" )
187+ else :
188+ discrepancy : Decimal = abs (calculated_billing_prediction - product_response_billing_prediction )
189+ if calculated_billing_prediction > product_response_billing_prediction :
190+ who_is_higher : str = "Calculated"
191+ else :
192+ who_is_higher : str = "Product response"
193+ console .log (":cross_mark: ERROR - Predictions do not match." )
194+ console .log (f"There's a discrepancy of { discrepancy } and the { who_is_higher } value is higher." )
195+ sys .exit (1 )
196+
156197
157198def _calculate_adjusted_coins (total_coins : Decimal ,
158199 allowance : Decimal ,
0 commit comments