forked from cursorman/coffer
205 lines
6.5 KiB
Python
205 lines
6.5 KiB
Python
"""Coffer sells its own goods for prepaid credit and pays suppliers by invoice."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
from coffer.ledger import Ledger
|
|
from coffer.units import ACCOUNT_COFFER, agent_account, to_subunits
|
|
|
|
|
|
class UnknownGood(Exception):
|
|
"""No good with that sku is sold by Coffer."""
|
|
|
|
|
|
class CannotSupply(Exception):
|
|
"""Coffer has no stock and no supplier it can order this good from."""
|
|
|
|
|
|
class DuplicateOrder(Exception):
|
|
"""This idempotency key was already used for a different order."""
|
|
|
|
|
|
@dataclass
|
|
class Supplier:
|
|
supplier_id: str
|
|
name: str
|
|
|
|
|
|
@dataclass
|
|
class Good:
|
|
sku: str
|
|
name: str
|
|
credit_price: int
|
|
supplier_id: str | None = None
|
|
supplier_unit_cost: int = 0
|
|
stock: int = 0
|
|
|
|
|
|
@dataclass
|
|
class Invoice:
|
|
invoice_id: str
|
|
supplier_id: str
|
|
sku: str
|
|
quantity: int
|
|
dollars: int
|
|
status: str = "payable"
|
|
|
|
|
|
@dataclass
|
|
class Order:
|
|
order_id: str
|
|
buyer_id: str
|
|
sku: str
|
|
quantity: int
|
|
credits_charged: int
|
|
draws: list[dict[str, int | str]]
|
|
from_stock: int
|
|
procured: int
|
|
status: str
|
|
invoice_id: str | None = None
|
|
tx_seq: int | None = None
|
|
|
|
|
|
@dataclass
|
|
class Store:
|
|
ledger: Ledger = field(default_factory=Ledger)
|
|
goods: dict[str, Good] = field(default_factory=dict)
|
|
suppliers: dict[str, Supplier] = field(default_factory=dict)
|
|
orders: dict[str, Order] = field(default_factory=dict)
|
|
invoices: dict[str, Invoice] = field(default_factory=dict)
|
|
merchant_account: str = ACCOUNT_COFFER
|
|
_seq: int = 0
|
|
_idempotency: dict[str, str] = field(default_factory=dict)
|
|
|
|
def add_supplier(self, supplier_id: str, name: str) -> Supplier:
|
|
if not supplier_id or not name:
|
|
raise ValueError("a supplier needs an id and a name")
|
|
supplier = Supplier(supplier_id, name)
|
|
self.suppliers[supplier_id] = supplier
|
|
return supplier
|
|
|
|
def add_good(
|
|
self,
|
|
sku: str,
|
|
name: str,
|
|
credit_price: int,
|
|
supplier_id: str | None = None,
|
|
supplier_unit_cost: int = 0,
|
|
stock: int = 0,
|
|
) -> Good:
|
|
if type(credit_price) is not int or credit_price <= 0:
|
|
raise ValueError("credit price must be a positive whole number of credits")
|
|
if type(supplier_unit_cost) is not int or supplier_unit_cost < 0:
|
|
raise ValueError("supplier cost must be a whole number of dollars")
|
|
if type(stock) is not int or stock < 0:
|
|
raise ValueError("stock must be a whole number")
|
|
if supplier_id is not None and supplier_id not in self.suppliers:
|
|
raise ValueError(f"unknown supplier {supplier_id}")
|
|
good = Good(sku, name, credit_price, supplier_id, supplier_unit_cost, stock)
|
|
self.goods[sku] = good
|
|
return good
|
|
|
|
def receive_stock(self, sku: str, quantity: int) -> Invoice | None:
|
|
"""Take goods into Coffer's own inventory. Pay the supplier by invoice, if there is one."""
|
|
if type(quantity) is not int or quantity <= 0:
|
|
raise ValueError("quantity must be a positive whole number")
|
|
good = self.goods.get(sku)
|
|
if good is None:
|
|
raise UnknownGood(sku)
|
|
good.stock += quantity
|
|
if good.supplier_id and good.supplier_unit_cost:
|
|
return self._invoice(good.supplier_id, sku, quantity, good.supplier_unit_cost * quantity)
|
|
return None
|
|
|
|
def place_order(
|
|
self,
|
|
buyer_id: str,
|
|
sku: str,
|
|
quantity: int = 1,
|
|
idempotency_key: str | None = None,
|
|
) -> Order:
|
|
"""Buyer pays Coffer in credit. Coffer owes the goods."""
|
|
buyer = agent_account(buyer_id)
|
|
if idempotency_key:
|
|
existing = self._idempotency.get(idempotency_key)
|
|
if existing is not None:
|
|
order = self.orders[existing]
|
|
if order.buyer_id != buyer or order.sku != sku or order.quantity != quantity:
|
|
raise DuplicateOrder(idempotency_key)
|
|
return order
|
|
if type(quantity) is not int or quantity <= 0:
|
|
raise ValueError("quantity must be a positive whole number")
|
|
good = self.goods.get(sku)
|
|
if good is None:
|
|
raise UnknownGood(sku)
|
|
from_stock = min(good.stock, quantity)
|
|
procured = quantity - from_stock
|
|
if procured and (not good.supplier_id or good.supplier_id not in self.suppliers):
|
|
raise CannotSupply(sku)
|
|
credits = good.credit_price * quantity
|
|
draws_sub, tx = self.ledger.purchase_settle(
|
|
buyer,
|
|
self.merchant_account,
|
|
to_subunits(credits),
|
|
reason=f"purchase:{sku}",
|
|
tx_kind="purchase",
|
|
meta={"sku": sku, "quantity": quantity},
|
|
goods=True, # every catalog item is a physical good: granted credit cannot pay
|
|
)
|
|
draws = [{"kind": d["kind"], "amount": int(d["amount"]) // 100} for d in draws_sub]
|
|
good.stock -= from_stock
|
|
invoice_id = None
|
|
if procured:
|
|
invoice = self._invoice(
|
|
good.supplier_id,
|
|
sku,
|
|
procured,
|
|
good.supplier_unit_cost * procured,
|
|
)
|
|
invoice_id = invoice.invoice_id
|
|
self._seq += 1
|
|
order = Order(
|
|
order_id=f"ord-{self._seq}",
|
|
buyer_id=buyer,
|
|
sku=sku,
|
|
quantity=quantity,
|
|
credits_charged=credits,
|
|
draws=draws,
|
|
from_stock=from_stock,
|
|
procured=procured,
|
|
status="fulfilled" if procured == 0 else "procuring",
|
|
invoice_id=invoice_id,
|
|
tx_seq=tx.seq,
|
|
)
|
|
self.orders[order.order_id] = order
|
|
if idempotency_key:
|
|
self._idempotency[idempotency_key] = order.order_id
|
|
return order
|
|
|
|
def _invoice(self, supplier_id: str, sku: str, quantity: int, dollars: int) -> Invoice:
|
|
self._seq += 1
|
|
invoice = Invoice(
|
|
invoice_id=f"inv-{self._seq}",
|
|
supplier_id=supplier_id,
|
|
sku=sku,
|
|
quantity=quantity,
|
|
dollars=dollars,
|
|
)
|
|
self.invoices[invoice.invoice_id] = invoice
|
|
return invoice
|
|
|
|
|
|
def new_store(ledger: Ledger | None = None) -> Store:
|
|
"""The opening catalog: a Coffer shirt at 20 credits."""
|
|
store = Store(ledger=ledger or Ledger())
|
|
store.add_supplier("printer", "Printer")
|
|
store.add_good(
|
|
sku="coffer-shirt",
|
|
name="Coffer shirt",
|
|
credit_price=20,
|
|
supplier_id="printer",
|
|
supplier_unit_cost=8,
|
|
)
|
|
return store
|