Compare commits

...

10 commits

7 changed files with 122 additions and 78 deletions

View file

@ -21,7 +21,7 @@ camisatoshi-wordpress-reports check-health
### Make a report for the UM agreement between two dates ### Make a report for the UM agreement between two dates
```shell ```shell
camisatoshi-wordpress-reports generate-um-report --start-date "2023-08-01T00:00:00" --end-date "2023-09-01T00:00:00"c camisatoshi-wordpress-reports generate-um-report --start-date "2023-08-01T00:00:00" --end-date "2023-09-01T00:00:00"
``` ```
This will generate a file named `report.csv` in the current working directory. This will generate a file named `report.csv` in the current working directory.
@ -37,4 +37,5 @@ This will generate a file named `report.csv` in the current working directory.
## Open issues ## Open issues
- Pagination is not being managed. The moment we have more than 100 orders, we are gonna run into issues. - Pagination is not being managed. The moment we have more than 100 orders, we are gonna run into issues.
- Some reports will break if the SKU has absolutely no orders. But well, if that happens, there is simply no report to build, so the only serious improvement would be dropping an informative error message.

View file

@ -21,10 +21,11 @@ def check_health():
def generate_sku_report( def generate_sku_report(
start_date: Annotated[datetime.datetime, typer.Option(prompt=True)], start_date: Annotated[datetime.datetime, typer.Option(prompt=True)],
end_date: Annotated[datetime.datetime, typer.Option(prompt=True)], end_date: Annotated[datetime.datetime, typer.Option(prompt=True)],
sku: Annotated[str, typer.Option(prompt=True)] sku: Annotated[str, typer.Option(prompt=True)],
): ):
controllers.generate_sku_report(start_date, end_date, sku) controllers.generate_sku_report(start_date, end_date, sku)
@app.command() @app.command()
def generate_um_report( def generate_um_report(
start_date: Annotated[datetime.datetime, typer.Option(prompt=True)], start_date: Annotated[datetime.datetime, typer.Option(prompt=True)],

View file

@ -6,23 +6,24 @@ DEFAULT_DOTENV_FILEPATH = ".camisatoshi-wordpress-reports/.env"
### Order keys ### Order keys
order_keys = SimpleNamespace() ORDER_KEYS = SimpleNamespace()
order_keys.meta_data = "meta_data" ORDER_KEYS.id = "id"
order_keys.total = "total" ORDER_KEYS.meta_data = "meta_data"
order_keys.line_items = "line_items" ORDER_KEYS.total = "total"
ORDER_KEYS.line_items = "line_items"
order_keys.line_item_keys = SimpleNamespace() ORDER_KEYS.line_item_keys = SimpleNamespace()
order_keys.line_item_keys.sku = "sku" ORDER_KEYS.line_item_keys.sku = "sku"
order_keys.line_item_keys.quantity = "quantity" ORDER_KEYS.line_item_keys.quantity = "quantity"
order_keys.line_item_keys.total = "total" ORDER_KEYS.line_item_keys.total = "total"
custom_meta_data_keys = SimpleNamespace() CUSTOM_META_DATA_KEYS = SimpleNamespace()
custom_meta_data_keys.is_settled_um = "is_settled_um" CUSTOM_META_DATA_KEYS.is_settled_um = "is_settled_um"
custom_meta_data_keys.sats_received = "sats_received" CUSTOM_META_DATA_KEYS.sats_received = "sats_received"
### Other ### Other
um_first_agreement_percentage = 0.5 UM_FIRST_AGREEMENT_PERCENTAGE = 0.5
bbo_royalty_fee = 0.2 BBO_ROYALTY_FEE_PERCENTAGE = 0.2
BBO_SKUS = ["TEE-05-BBO-BLACK", "SUD-01-BBO-BLACK"]

View file

@ -9,12 +9,14 @@ from woocommerce import API
from camisatoshi_wordpress_reports.order import Order, Orders from camisatoshi_wordpress_reports.order import Order, Orders
from camisatoshi_wordpress_reports.constants import ( from camisatoshi_wordpress_reports.constants import (
um_first_agreement_percentage, ORDER_KEYS,
UM_FIRST_AGREEMENT_PERCENTAGE,
DEFAULT_DOTENV_FILEPATH, DEFAULT_DOTENV_FILEPATH,
bbo_royalty_fee, BBO_ROYALTY_FEE_PERCENTAGE,
BBO_SKUS,
) )
from camisatoshi_wordpress_reports.report_building import ( from camisatoshi_wordpress_reports.report_building import (
ReportChainBuilder, OrderObtentionChainBuilder,
WoocomerceOrderScope, WoocomerceOrderScope,
keep_orders_containing_sku, keep_orders_containing_sku,
) )
@ -72,10 +74,14 @@ def generate_um_report(
) )
logger.info(f"Received {len(orders_in_date_range)} orders.") logger.info(f"Received {len(orders_in_date_range)} orders.")
relevant_sku = "TEE-05-BBO-BLACK" relevant_skus = [
logger.info(f"Filtering by SKU: {relevant_sku}") "TEE-05-BBO-BLACK",
relevant_orders = orders_in_date_range.filter_orders_by_sku( "SUD-01-BBO-BLACK",
sku=relevant_sku "TEE-09-SIMPLY-BITCOIN",
]
logger.info(f"Filtering by SKUs: {relevant_skus}")
relevant_orders = orders_in_date_range.filter_orders_by_skus(
skus=relevant_skus
) )
logger.info(f"Kept {len(relevant_orders)} orders.") logger.info(f"Kept {len(relevant_orders)} orders.")
@ -106,21 +112,35 @@ def generate_um_report(
f"Relevant orders: {[order['id'] for order in unsettled_orders]}." f"Relevant orders: {[order['id'] for order in unsettled_orders]}."
) )
report = [] report = []
for order in unsettled_orders: for relevant_sku in relevant_skus:
report.append( logger.debug(f"Reporting SKU {relevant_sku}")
{ for order in unsettled_orders:
"order_id": order["id"], if not order.contains_sku(relevant_sku):
"sku": relevant_sku, continue
"units_sold": order.units_of_sku(relevant_sku), logger.debug(f"Reporting for order {order[ORDER_KEYS.id]}")
"eur_income": order.sales_of_sku(relevant_sku),
"sats_income": order.sats_received_for_sku(relevant_sku), # A few helper variables to make the last variable more understandable
"sats_owed_to_um": ( sats_received_for_sku = order.sats_received_for_sku(relevant_sku)
order.sats_received_for_sku(relevant_sku) bbo_fee_if_applicable = (BBO_ROYALTY_FEE_PERCENTAGE * (relevant_sku in BBO_SKUS))
* (1 - bbo_royalty_fee) discount_factor_for_bbo_skus = 1 - bbo_fee_if_applicable
)
* um_first_agreement_percentage, # We owe UM his percentages of the sats received after discounting
} # royalties paid to BBO for the BBO products.
) sats_owed_to_um = (
(sats_received_for_sku * discount_factor_for_bbo_skus)
* UM_FIRST_AGREEMENT_PERCENTAGE
)
report.append(
{
"order_id": order[ORDER_KEYS.id],
"sku": relevant_sku,
"units_sold": order.units_of_sku(relevant_sku),
"eur_income": order.sales_of_sku(relevant_sku),
"sats_income": sats_received_for_sku,
"sats_owed_to_um": sats_owed_to_um,
}
)
logger.info("Report generated.") logger.info("Report generated.")
logger.info(report) logger.info(report)
@ -131,10 +151,11 @@ def generate_um_report(
dict_writer.writeheader() dict_writer.writeheader()
dict_writer.writerows(report) dict_writer.writerows(report)
def generate_sku_report(start_date, end_date, sku): def generate_sku_report(start_date, end_date, sku):
logger.info(f"Fetching orders between {start_date} and {end_date}.") logger.info(f"Fetching orders between {start_date} and {end_date}.")
report_chain_builder = ReportChainBuilder() report_chain_builder = OrderObtentionChainBuilder()
report_chain_builder.add_order_fetching_step( report_chain_builder.add_order_fetching_step(
wc_order_scope=WoocomerceOrderScope( wc_order_scope=WoocomerceOrderScope(
@ -156,10 +177,10 @@ def generate_sku_report(start_date, end_date, sku):
for order in relevant_orders: for order in relevant_orders:
report.append( report.append(
{ {
"order_id": order["id"], "order_id": order[ORDER_KEYS.id],
"sku": sku, "sku": sku,
"units_sold": order.units_of_sku(sku), "units_sold": order.units_of_skus(sku),
"eur_income": order.sales_of_sku(sku), "eur_income": order.sales_of_skus(sku),
} }
) )
logger.info("Report generated.") logger.info("Report generated.")

View file

@ -1,4 +1,5 @@
import logging import logging
def set_config_level(): def set_config_level():
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)

View file

@ -1,12 +1,13 @@
import logging import logging
from typing import Dict, Collection from typing import Dict, Collection
from camisatoshi_wordpress_reports.constants import order_keys, custom_meta_data_keys from camisatoshi_wordpress_reports.constants import (
ORDER_KEYS,
CUSTOM_META_DATA_KEYS,
)
from camisatoshi_wordpress_reports.utils import safe_zero_division from camisatoshi_wordpress_reports.utils import safe_zero_division
logger = logging.getLogger()
class Order: class Order:
def __init__(self, raw_data: Dict): def __init__(self, raw_data: Dict):
self.raw_data = raw_data self.raw_data = raw_data
@ -18,30 +19,32 @@ class Order:
def meta_data_entries(self): def meta_data_entries(self):
return { return {
meta_data_entry["key"]: meta_data_entry["value"] meta_data_entry["key"]: meta_data_entry["value"]
for meta_data_entry in self.raw_data[order_keys.meta_data] for meta_data_entry in self.raw_data[ORDER_KEYS.meta_data]
} }
def units_of_sku(self, sku: str) -> int: def units_of_sku(self, sku: str) -> int:
units = 0 units = 0
for line in self[order_keys.line_items]: for line in self[ORDER_KEYS.line_items]:
if line[order_keys.line_item_keys.sku] == sku: if line[ORDER_KEYS.line_item_keys.sku] == sku:
units += line[order_keys.line_item_keys.quantity] units += line[ORDER_KEYS.line_item_keys.quantity]
return units return units
def sales_of_sku(self, sku: str) -> float: def sales_of_sku(self, sku: str) -> float:
sales = 0 sales = 0
for line in self[order_keys.line_items]: for line in self[ORDER_KEYS.line_items]:
if line[order_keys.line_item_keys.sku] == sku: if line[ORDER_KEYS.line_item_keys.sku] == sku:
sales += float(line[order_keys.line_item_keys.total]) sales += float(line[ORDER_KEYS.line_item_keys.total])
return sales return sales
def sats_received_for_sku(self, sku: str) -> float: def sats_received_for_sku(self, sku: str) -> float:
total_order_eur = float(self[order_keys.total]) total_order_eur = float(self[ORDER_KEYS.total])
eur_of_sku = self.sales_of_sku(sku) eur_of_sku = self.sales_of_sku(sku)
monetary_weight_of_sku_in_order = safe_zero_division(eur_of_sku, total_order_eur) monetary_weight_of_sku_in_order = safe_zero_division(
eur_of_sku, total_order_eur
)
total_order_sats_received = float( total_order_sats_received = float(
self.meta_data_entries[custom_meta_data_keys.sats_received] self.meta_data_entries[CUSTOM_META_DATA_KEYS.sats_received]
) )
sats_received_for_sku = ( sats_received_for_sku = (
monetary_weight_of_sku_in_order * total_order_sats_received monetary_weight_of_sku_in_order * total_order_sats_received
@ -50,12 +53,9 @@ class Order:
return sats_received_for_sku return sats_received_for_sku
def contains_sku(self, sku: str) -> bool: def contains_sku(self, sku: str) -> bool:
logger.debug(f"Checking if order {self['id']} contains sku {sku}.") for item in self[ORDER_KEYS.line_items]:
for item in self[order_keys.line_items]: if item[ORDER_KEYS.line_item_keys.sku] == sku:
if item[order_keys.line_item_keys.sku] == sku:
logger.debug(f"It does.")
return True return True
logger.debug("It doesn't.")
return False return False
def contains_meta_data_entry(self, meta_data_entry_key: str) -> bool: def contains_meta_data_entry(self, meta_data_entry_key: str) -> bool:
@ -65,7 +65,7 @@ class Order:
def is_settled_with_um(self): def is_settled_with_um(self):
is_settled = self.meta_data_entries.get( is_settled = self.meta_data_entries.get(
custom_meta_data_keys.is_settled_um, None CUSTOM_META_DATA_KEYS.is_settled_um, None
) )
return bool(is_settled) return bool(is_settled)
@ -93,6 +93,9 @@ class Orders:
return next_order return next_order
raise StopIteration raise StopIteration
def __repr__(self):
return str([f"Order {order['id']}" for order in self])
def filter_orders_by_sku(self, sku: str) -> "Orders": def filter_orders_by_sku(self, sku: str) -> "Orders":
filtered_orders = [] filtered_orders = []
@ -100,7 +103,18 @@ class Orders:
if order.contains_sku(sku): if order.contains_sku(sku):
filtered_orders.append(order) filtered_orders.append(order)
logger.debug(f"Finished filtering. I have {len(filtered_orders)} orders.") return Orders(filtered_orders)
def filter_orders_by_skus(self, skus: Collection[str]) -> "Orders":
filtered_orders = []
for order in self:
order_contains_at_least_one_of_the_skus = None # Guilty until proven innocent
for sku in skus:
if order.contains_sku(sku):
order_contains_at_least_one_of_the_skus = True
if order_contains_at_least_one_of_the_skus:
filtered_orders.append(order)
return Orders(filtered_orders) return Orders(filtered_orders)
@ -108,7 +122,9 @@ class Orders:
orders_without_sats_received = [] orders_without_sats_received = []
for order in self: for order in self:
if not order.contains_meta_data_entry(custom_meta_data_keys.sats_received): if not order.contains_meta_data_entry(
CUSTOM_META_DATA_KEYS.sats_received
):
orders_without_sats_received.append(order) orders_without_sats_received.append(order)
continue continue
@ -127,7 +143,10 @@ class Orders:
orders_with_metadata_value = [] orders_with_metadata_value = []
for order in self: for order in self:
if order.contains_meta_data_entry(key) and order.meta_data_entries[key] == value: if (
order.contains_meta_data_entry(key)
and order.meta_data_entries[key] == value
):
orders_with_metadata_value.append(order) orders_with_metadata_value.append(order)
return Orders(orders_with_metadata_value) return Orders(orders_with_metadata_value)

View file

@ -84,7 +84,7 @@ def validate_orders_satisfy_metadata_filter(
return True return True
class ReportChain: class OrderObtentionChain:
def __init__(self): def __init__(self):
self.order_fetching_step = None self.order_fetching_step = None
self.order_filtering_steps = [] self.order_filtering_steps = []
@ -106,7 +106,7 @@ class ReportChain:
def _run_order_fetching_step(self, wc_api_client: API) -> Orders: def _run_order_fetching_step(self, wc_api_client: API) -> Orders:
logger.debug("Running order fetching step.") logger.debug("Running order fetching step.")
self._orders = self.order_fetching_step(wc_api_client) self._orders = self.order_fetching_step(wc_api_client)
logger.debug(f"Received {self._orders} orders.") logger.debug(f"Received {len(self._orders)} orders.")
def _run_order_filtering_steps(self) -> Orders: def _run_order_filtering_steps(self) -> Orders:
logger.debug( logger.debug(
@ -127,37 +127,37 @@ class ReportChain:
raise ValueError("Error during order validation step.") raise ValueError("Error during order validation step.")
class ReportChainBuilder: class OrderObtentionChainBuilder:
def __init__(self): def __init__(self):
self._wip_report_chain = ReportChain() self._wip_order_obtention_chain = OrderObtentionChain()
def add_order_fetching_step( def add_order_fetching_step(
self, wc_order_scope: WoocomerceOrderScope self, wc_order_scope: WoocomerceOrderScope
) -> "ReportChainBuilder": ) -> "OrderObtentionChainBuilder":
""" """
Define a scope of orders to get from Woocomerce. Define a scope of orders to get from Woocomerce.
""" """
self._order_scope = wc_order_scope self._order_scope = wc_order_scope
self._wip_report_chain.order_fetching_step = partial( self._wip_order_obtention_chain.order_fetching_step = partial(
fetch_orders_from_wc, wc_order_scope=wc_order_scope fetch_orders_from_wc, wc_order_scope=wc_order_scope
) )
return self return self
def add_order_filtering_step(self, step) -> "ReportChainBuilder": def add_order_filtering_step(self, step) -> "OrderObtentionChainBuilder":
self._wip_report_chain.order_filtering_steps.append(step) self._wip_order_obtention_chain.order_filtering_steps.append(step)
return self return self
def add_order_validation_step(self, step) -> "ReportChainBuilder": def add_order_validation_step(self, step) -> "OrderObtentionChainBuilder":
self._wip_report_chain.order_validation_steps.append(step) self._wip_order_obtention_chain.order_validation_steps.append(step)
return self return self
def get_report_chain(self) -> ReportChain: def get_report_chain(self) -> OrderObtentionChain:
return self._wip_report_chain return self._wip_order_obtention_chain