|
| 1 | +#!/usr/bin/env python |
| 2 | +"""Calculates an Invoice for an AS Product. |
| 3 | +""" |
| 4 | +import argparse |
| 5 | +from decimal import Decimal |
| 6 | +from collections import namedtuple |
| 7 | +from typing import Any, Dict, Optional |
| 8 | +import urllib3 |
| 9 | + |
| 10 | +from rich.pretty import pprint |
| 11 | +from squonk2.auth import Auth |
| 12 | +from squonk2.as_api import AsApi, AsApiRv |
| 13 | + |
| 14 | +from common import Env, get_env |
| 15 | + |
| 16 | +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) |
| 17 | + |
| 18 | +AdjustedCoins: namedtuple = namedtuple("AdjustedCoins", |
| 19 | + ["coins", "fc", "ac", "aac", "lc", "alc"]) |
| 20 | + |
| 21 | + |
| 22 | +def main(c_args: argparse.Namespace) -> None: |
| 23 | + """Main function.""" |
| 24 | + env: Optional[Env] = get_env() |
| 25 | + if not env: |
| 26 | + return |
| 27 | + |
| 28 | + token: str = Auth.get_access_token( |
| 29 | + keycloak_url=env.keycloak_url, |
| 30 | + keycloak_realm=env.keycloak_realm, |
| 31 | + keycloak_client_id=env.keycloak_as_client_id, |
| 32 | + username=env.keycloak_user, |
| 33 | + password=env.keycloak_user_password, |
| 34 | + ) |
| 35 | + |
| 36 | + # Get the product details. |
| 37 | + # This gives us the product's allowance, limit and overspend multipliers |
| 38 | + p_rv: AsApiRv = AsApi.get_product(token, product_id=args.product) |
| 39 | + assert p_rv.success |
| 40 | + allowance: Decimal = Decimal(p_rv.msg["product"]["coins"]["allowance"]) |
| 41 | + allowance_multiplier: Decimal = Decimal(p_rv.msg["product"]["coins"]["allowance_multiplier"]) |
| 42 | + limit: Decimal = Decimal(p_rv.msg["product"]["coins"]["limit"]) |
| 43 | + overspend_multiplier: Decimal = Decimal(p_rv.msg["product"]["coins"]["overspend_multiplier"]) |
| 44 | + |
| 45 | + remaining_days: int = p_rv.msg["product"]["coins"]["remaining_days"] |
| 46 | + |
| 47 | + invoice: Dict[str, Any] = { |
| 48 | + "Allowance": str(allowance), |
| 49 | + "Allowance Multiplier": str(allowance_multiplier), |
| 50 | + "Limit": str(limit), |
| 51 | + "Overspend Multiplier": str(overspend_multiplier), |
| 52 | + } |
| 53 | + |
| 54 | + # Get the product's charges... |
| 55 | + pc_rv: AsApiRv = AsApi.get_product_charges(token, product_id=args.product) |
| 56 | + assert pc_rv.success |
| 57 | + invoice["From"] = pc_rv.msg["from"] |
| 58 | + invoice["Until"] = pc_rv.msg["until"] |
| 59 | + |
| 60 | +# pprint(pc_rv.msg) |
| 61 | + |
| 62 | + # Accumulate all the storage costs |
| 63 | + # (excluding the current which will be interpreted as the "burn rate") |
| 64 | + num_storage_charges: int = 0 |
| 65 | + burn_rate: Decimal = Decimal() |
| 66 | + total_storage_coins: Decimal = Decimal() |
| 67 | + if "items" in pc_rv.msg["storage_charges"]: |
| 68 | + for item in pc_rv.msg["storage_charges"]["items"]: |
| 69 | + if "current_bytes" in item["additional_data"]: |
| 70 | + burn_rate = Decimal(item["coins"]) |
| 71 | + else: |
| 72 | + total_storage_coins += Decimal(item["coins"]) |
| 73 | + num_storage_charges += 1 |
| 74 | + |
| 75 | + # Accumulate all the processing costs |
| 76 | + num_processing_charges: int = 0 |
| 77 | + total_processing_coins: Decimal = Decimal() |
| 78 | + if pc_rv.msg["processing_charges"]: |
| 79 | + for merchant in pc_rv.msg["processing_charges"]: |
| 80 | + for item in merchant["items"]: |
| 81 | + total_processing_coins += Decimal(item["coins"]) |
| 82 | + num_processing_charges += 1 |
| 83 | + |
| 84 | + # Accumulate processing coins |
| 85 | + total_processing_coins: Decimal = Decimal() |
| 86 | + |
| 87 | + invoice["Billing Day"] = p_rv.msg["product"]["coins"]["billing_day"] |
| 88 | + invoice["Remaining Days"] = remaining_days |
| 89 | + invoice["Current Burn Rate"] = str(burn_rate) |
| 90 | + invoice["Number of Storage Charges"] = num_storage_charges |
| 91 | + invoice["Accrued Storage Coins"] = str(total_storage_coins) |
| 92 | + invoice["Number of Processing Charges"] = num_processing_charges |
| 93 | + invoice["Accrued Processing Coins"] = str(total_processing_coins) |
| 94 | + |
| 95 | + total_coins: Decimal = total_storage_coins + total_processing_coins |
| 96 | + |
| 97 | + ac: AdjustedCoins = _calculate_adjusted_coins( |
| 98 | + total_coins, |
| 99 | + allowance, |
| 100 | + allowance_multiplier, |
| 101 | + limit, |
| 102 | + overspend_multiplier |
| 103 | + ) |
| 104 | + |
| 105 | + invoice["Overspend Adjustment"] = { |
| 106 | + "Coins (Total Raw)": f"{total_storage_coins} + {total_processing_coins} = {total_coins}", |
| 107 | + "Coins (Penalty Free)": str(ac.fc), |
| 108 | + "Coins (In Allowance Band)": str(ac.ac), |
| 109 | + "Coins (Allowance Charge)": f"{ac.ac} x {allowance_multiplier} = {ac.aac}", |
| 110 | + "Coins (Above Limit)": str(ac.lc), |
| 111 | + "Coins (Overspend Charge)": f"{ac.lc} x {overspend_multiplier} = {ac.alc}", |
| 112 | + "Coins (Adjusted)": f"{ac.fc} + {ac.aac} + {ac.alc} = {ac.coins}", |
| 113 | + } |
| 114 | + |
| 115 | + additional_coins: Decimal = burn_rate * remaining_days |
| 116 | + predicted_total_coins: Decimal = total_coins |
| 117 | + zero: Decimal = Decimal() |
| 118 | + if remaining_days > 0: |
| 119 | + if burn_rate > zero: |
| 120 | + |
| 121 | + predicted_total_coins += additional_coins |
| 122 | + p_ac: AdjustedCoins = _calculate_adjusted_coins( |
| 123 | + predicted_total_coins, |
| 124 | + allowance, |
| 125 | + allowance_multiplier, |
| 126 | + limit, |
| 127 | + overspend_multiplier) |
| 128 | + |
| 129 | + invoice["Predicted Overspend Adjustment"] = { |
| 130 | + "Coins (Burn Rate)": str(burn_rate), |
| 131 | + "Coins (Additional Spend)": f"{remaining_days} x {burn_rate} = {additional_coins}", |
| 132 | + "Coins (Total Raw)": f"{total_coins} + {additional_coins} = {predicted_total_coins}", |
| 133 | + "Coins (Penalty Free)": str(p_ac.fc), |
| 134 | + "Coins (In Allowance Band)": str(p_ac.ac), |
| 135 | + "Coins (Allowance Charge)": f"{p_ac.ac} x {allowance_multiplier} = {p_ac.aac}", |
| 136 | + "Coins (Above Limit)": str(p_ac.lc), |
| 137 | + "Coins (Overspend Charge)": f"{p_ac.lc} x {overspend_multiplier} = {p_ac.alc}", |
| 138 | + "Coins (Adjusted)": f"{p_ac.fc} + {p_ac.aac} + {p_ac.alc} = {p_ac.coins}", |
| 139 | + } |
| 140 | + |
| 141 | + # Now just pre-tty-print the invoice |
| 142 | + pprint(invoice) |
| 143 | + |
| 144 | + |
| 145 | +def _calculate_adjusted_coins(total_coins: Decimal, |
| 146 | + allowance: Decimal, |
| 147 | + allowance_multiplier: Decimal, |
| 148 | + limit: Decimal, |
| 149 | + overspend_multiplier: Decimal) -> AdjustedCoins: |
| 150 | + """Adjust total based on allowance and limit multipliers. |
| 151 | + Coins between the allowance and limit use the allowance multiplier. |
| 152 | + Coins above the limit use the limit multiplier. |
| 153 | + """ |
| 154 | + |
| 155 | + # How many are free of any penalty? |
| 156 | + free_coins: Decimal = min(total_coins, allowance) |
| 157 | + |
| 158 | + allowance_coins: Decimal = Decimal() |
| 159 | + adjusted_allowance_coins: Decimal = Decimal() |
| 160 | + limit_coins: Decimal = Decimal() |
| 161 | + adjusted_limit_coins: Decimal = Decimal() |
| 162 | + |
| 163 | + allowance_band: Decimal = limit - allowance |
| 164 | + |
| 165 | + if total_coins > allowance: |
| 166 | + allowance_coins = min(total_coins - allowance, allowance_band) |
| 167 | + adjusted_allowance_coins = allowance_coins * allowance_multiplier |
| 168 | + if total_coins > limit: |
| 169 | + limit_coins = total_coins - limit |
| 170 | + adjusted_limit_coins = limit_coins * overspend_multiplier |
| 171 | + |
| 172 | + adjusted_coins: Decimal = free_coins + adjusted_allowance_coins + adjusted_limit_coins |
| 173 | + |
| 174 | + return AdjustedCoins(coins=adjusted_coins, |
| 175 | + fc=free_coins, |
| 176 | + ac=allowance_coins, |
| 177 | + aac=adjusted_allowance_coins, |
| 178 | + lc=limit_coins, |
| 179 | + alc=adjusted_limit_coins) |
| 180 | + |
| 181 | + |
| 182 | +if __name__ == "__main__": |
| 183 | + |
| 184 | + # Parse command line arguments |
| 185 | + parser = argparse.ArgumentParser( |
| 186 | + description="Calculates a Product's Invoice (actual and predicted)" |
| 187 | + ) |
| 188 | + parser.add_argument('product', type=str, help='The Product UUID') |
| 189 | + args: argparse.Namespace = parser.parse_args() |
| 190 | + |
| 191 | + main(args) |
0 commit comments