Skip to content

Commit 9fded85

Browse files
author
Alan Christie
committed
Adds invoice.py
Updated requirements
1 parent b4db278 commit 9fded85

File tree

2 files changed

+192
-1
lines changed

2 files changed

+192
-1
lines changed

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
im-squonk2-client ~= 1.0
1+
im-squonk2-client >= 1.9.0, < 2.0.0
22
python-dateutil == 2.8.2
33
rich == 12.5.1

tools/invoice.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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

Comments
 (0)