coffer/coffer/product.py

676 lines
28 KiB
Python

"""Durable Coffer product: wallets, ledger txs, signup, subscription, catalog."""
from __future__ import annotations
import contextlib
import math
from dataclasses import asdict
from datetime import datetime
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo
from coffer.db import CofferDB
from coffer.public_ledger import snapshot_from_product
from coffer.ledger import CreditKind, Ledger, Lot, Tx
from coffer.store import ACCOUNT_ESCROW, ORDER_REVIEW_NOTICE, Good, Invoice, Order, new_store
from coffer.units import (
ACCOUNT_COFFER,
ACCOUNT_LEORA,
ACCOUNT_NEW_INTERNET,
COFFER_INVESTMENT_CREDITS,
CONTRACT_CREDITS,
LEORA_FUNDED_AFTER_GRANTS_CREDITS,
SCALE,
SHIRT_CREDITS,
SIGNUP_GRANT_CREDITS,
SUBSCRIPTION_CREDITS,
SUBSCRIPTION_CUT_BPS,
MAX_PAYMENT_CREDITS,
account_kind,
agent_account,
human_account,
cut_subunits,
format_credits,
to_subunits,
)
PT = ZoneInfo("Australia/Perth")
def now_pt() -> str:
return datetime.now(tz=PT).isoformat(timespec="seconds")
class ProductError(Exception):
"""Product-level error."""
class CofferProduct:
"""High-level API the platform and CLI call."""
def __init__(self, db_path: str | Path) -> None:
self.db = CofferDB(db_path)
self.ledger = Ledger()
self.store = new_store(self.ledger)
self._load()
def close(self) -> None:
self.db.close()
def _load(self) -> None:
self.ledger = Ledger()
self.store = new_store(self.ledger)
self.store.suppliers.clear()
self.store.goods.clear()
for row in self.db.execute("SELECT account_id FROM accounts").fetchall():
self.ledger.ensure_wallet(row["account_id"])
for row in self.db.execute(
"SELECT account_id, kind, remaining, reason FROM lots "
"WHERE remaining > 0 ORDER BY lot_id"
).fetchall():
self.ledger.account(row["account_id"]).lots.append(
Lot(CreditKind(row["kind"]), int(row["remaining"]), row["reason"])
)
self.ledger.transactions.clear()
self.ledger._tx_seq = 0
for row in self.db.execute(
"SELECT seq, kind, amount, debit_account, credit_account, reason, meta_json "
"FROM transactions ORDER BY seq"
).fetchall():
tx = Tx(
seq=int(row["seq"]),
kind=row["kind"],
amount=int(row["amount"]),
debit_account=row["debit_account"],
credit_account=row["credit_account"],
reason=row["reason"],
meta=self.db.loads(row["meta_json"] or "{}"),
)
self.ledger.transactions.append(tx)
self.ledger._tx_seq = max(self.ledger._tx_seq, tx.seq)
for row in self.db.execute("SELECT supplier_id, name FROM suppliers").fetchall():
self.store.add_supplier(row["supplier_id"], row["name"])
for row in self.db.execute(
"SELECT sku, name, credit_price, supplier_id, supplier_unit_cost, stock FROM goods"
).fetchall():
self.store.goods[row["sku"]] = Good(
sku=row["sku"],
name=row["name"],
credit_price=int(row["credit_price"]),
supplier_id=row["supplier_id"],
supplier_unit_cost=int(row["supplier_unit_cost"]),
stock=int(row["stock"]),
)
if "coffer-shirt" not in self.store.goods:
if "printer" not in self.store.suppliers:
self.store.add_supplier("printer", "Printer")
self.store.add_good(
sku="coffer-shirt",
name="Coffer shirt",
credit_price=SHIRT_CREDITS,
supplier_id="printer",
supplier_unit_cost=8,
)
self._persist_catalog()
self.db.commit()
for row in self.db.execute("SELECT * FROM invoices").fetchall():
inv = Invoice(
invoice_id=row["invoice_id"],
supplier_id=row["supplier_id"],
sku=row["sku"],
quantity=int(row["quantity"]),
dollars=int(row["dollars"]),
status=row["status"],
)
self.store.invoices[inv.invoice_id] = inv
self._bump_seq_from_id(inv.invoice_id)
for row in self.db.execute("SELECT * FROM orders").fetchall():
order = Order(
order_id=row["order_id"],
buyer_id=row["buyer_id"],
sku=row["sku"],
quantity=int(row["quantity"]),
credits_charged=int(row["credits_charged"]),
draws=self.db.loads(row["draws_json"]),
from_stock=int(row["from_stock"]),
procured=int(row["procured"]),
status=row["status"],
invoice_id=row["invoice_id"],
tx_seq=row["tx_seq"],
held=int(row["held"] or 0),
hold_tx_seq=row["hold_tx_seq"],
reviewed_by=row["reviewed_by"],
reviewed_at=row["reviewed_at"],
review_note=row["review_note"],
)
self.store.orders[order.order_id] = order
self._bump_seq_from_id(order.order_id)
for row in self.db.execute("SELECT key, order_id FROM idempotency").fetchall():
self.store._idempotency[row["key"]] = row["order_id"]
def _bump_seq_from_id(self, oid: str) -> None:
try:
n = int(str(oid).rsplit("-", 1)[-1])
if n > self.store._seq:
self.store._seq = n
except ValueError:
pass
def _ensure_account_row(self, account_id: str) -> None:
kind = account_kind(account_id)
self.db.execute(
"INSERT OR IGNORE INTO accounts(account_id, kind, created_at) VALUES(?,?,?)",
(account_id, kind, now_pt()),
)
def _rewrite_lots(self, account_id: str) -> None:
self._ensure_account_row(account_id)
self.db.execute("DELETE FROM lots WHERE account_id=?", (account_id,))
for lot in self.ledger.account(account_id).lots:
if lot.remaining <= 0:
continue
self.db.execute(
"INSERT INTO lots(account_id, kind, remaining, reason, created_at) "
"VALUES(?,?,?,?,?)",
(account_id, lot.kind.value, lot.remaining, lot.reason, now_pt()),
)
def _persist_tx(self, tx: Tx) -> None:
self.db.execute(
"INSERT INTO transactions(seq, kind, amount, debit_account, credit_account, "
"reason, meta_json, created_at) VALUES(?,?,?,?,?,?,?,?)",
(
tx.seq,
tx.kind,
tx.amount,
tx.debit_account,
tx.credit_account,
tx.reason,
self.db.dumps(tx.meta),
now_pt(),
),
)
def _persist_catalog(self) -> None:
for s in self.store.suppliers.values():
self.db.execute(
"INSERT OR REPLACE INTO suppliers(supplier_id, name) VALUES(?,?)",
(s.supplier_id, s.name),
)
for g in self.store.goods.values():
self.db.execute(
"INSERT OR REPLACE INTO goods(sku, name, credit_price, supplier_id, "
"supplier_unit_cost, stock) VALUES(?,?,?,?,?,?)",
(
g.sku,
g.name,
g.credit_price,
g.supplier_id,
g.supplier_unit_cost,
g.stock,
),
)
def _persist_order(self, order: Order) -> None:
self.db.execute(
"INSERT OR REPLACE INTO orders(order_id, buyer_id, sku, quantity, "
"credits_charged, draws_json, from_stock, procured, status, invoice_id, "
"tx_seq, created_at, held, hold_tx_seq, reviewed_by, reviewed_at, review_note) "
"VALUES(?,?,?,?,?,?,?,?,?,?,?,COALESCE((SELECT created_at FROM orders WHERE order_id=?),?),?,?,?,?,?)",
(
order.order_id,
order.buyer_id,
order.sku,
order.quantity,
order.credits_charged,
self.db.dumps(order.draws),
order.from_stock,
order.procured,
order.status,
order.invoice_id,
order.tx_seq,
order.order_id,
now_pt(),
order.held,
order.hold_tx_seq,
order.reviewed_by,
order.reviewed_at,
order.review_note,
),
)
if order.invoice_id and order.invoice_id in self.store.invoices:
inv = self.store.invoices[order.invoice_id]
self.db.execute(
"INSERT OR REPLACE INTO invoices(invoice_id, supplier_id, sku, quantity, "
"dollars, status, created_at) VALUES(?,?,?,?,?,?,?)",
(
inv.invoice_id,
inv.supplier_id,
inv.sku,
inv.quantity,
inv.dollars,
inv.status,
now_pt(),
),
)
self._persist_catalog()
def _commit_accounts(self, *account_ids: str, txs: list[Tx] | None = None) -> None:
seen: set[str] = set()
for aid in account_ids:
if aid and aid not in seen:
self._rewrite_lots(aid)
seen.add(aid)
for tx in txs or []:
self._persist_tx(tx)
self.db.commit()
if txs:
self._after_mutation()
# ----- public API ----------------------------------------------------------
def create_wallet(self, account_id: str) -> dict[str, Any]:
aid = human_account(account_id) if account_id.startswith("human:") else agent_account(account_id)
self.ledger.ensure_wallet(aid)
self._ensure_account_row(aid)
self.db.commit()
return {"account_id": aid, "balance": self.balance(aid)}
def balance(self, account_id: str) -> dict[str, Any]:
aid = human_account(account_id) if account_id.startswith("human:") else agent_account(account_id)
raw = self.ledger.balance_subunits(aid)
return {
"account_id": aid,
"promotional": raw["promotional"] / SCALE,
"funded": raw["funded"] / SCALE,
"earned": raw["earned"] / SCALE,
"spendable": raw["spendable"] / SCALE,
"held": self.store.held_for(aid) / SCALE, # orders waiting for review (not charged)
"granted": raw["promotional"] / SCALE, # alias: free signup-grant credit
"promotional_centicredits": raw["promotional"],
"funded_centicredits": raw["funded"],
"earned_centicredits": raw["earned"],
"spendable_centicredits": raw["spendable"],
}
def history(self) -> list[dict[str, Any]]:
return [self.ledger.describe_tx(tx) for tx in self.ledger.history()]
def bootstrap_opening_books(self) -> dict[str, Any]:
"""Seed opening balances without business txs (so contract can be first tx)."""
if self.db.get_meta("bootstrapped") == "1":
return {"bootstrapped": True, "already": True}
before = len(self.ledger.transactions)
self.ledger.bootstrap_endowment(
ACCOUNT_COFFER,
COFFER_INVESTMENT_CREDITS,
CreditKind.FUNDED,
"leora-investment",
)
self.ledger.bootstrap_endowment(
ACCOUNT_LEORA,
LEORA_FUNDED_AFTER_GRANTS_CREDITS,
CreditKind.FUNDED,
"funded-after-grants",
)
self.ledger.ensure_wallet(ACCOUNT_NEW_INTERNET)
self._rewrite_lots(ACCOUNT_COFFER)
self._rewrite_lots(ACCOUNT_LEORA)
self._ensure_account_row(ACCOUNT_NEW_INTERNET)
self.db.set_meta("bootstrapped", "1")
self.db.commit()
assert len(self.ledger.transactions) == before
return {
"bootstrapped": True,
"already": False,
"coffer": self.balance(ACCOUNT_COFFER),
"leora": self.balance(ACCOUNT_LEORA),
}
def record_build_contract(
self,
amount_credits: int = CONTRACT_CREDITS,
*,
payer: str = ACCOUNT_COFFER,
payee: str = ACCOUNT_LEORA,
) -> dict[str, Any]:
"""First business ledger tx: Coffer pays Leora for the product build."""
if self.db.get_meta("contract_recorded") == "1":
for tx in self.ledger.history():
if tx.kind == "contract_payment":
return {"already": True, "tx": self.ledger.describe_tx(tx)}
raise ProductError("contract_recorded meta set but tx missing")
if self.db.get_meta("bootstrapped") != "1":
self.bootstrap_opening_books()
tx = self.ledger.transfer(
payer,
payee,
to_subunits(amount_credits),
reason="build-contract:leora-coffer-product",
tx_kind="contract_payment",
credit_kind=CreditKind.FUNDED,
meta={"contract": "coffer-product-build", "credits": amount_credits},
)
self._commit_accounts(payer, payee, txs=[tx])
self.db.set_meta("contract_recorded", "1")
self.db.commit()
return {
"already": False,
"tx": self.ledger.describe_tx(tx),
"payer": self.balance(payer),
"payee": self.balance(payee),
}
def grant_signup(
self,
agent_id: str,
amount_credits: int = SIGNUP_GRANT_CREDITS,
) -> dict[str, Any]:
aid = agent_account(agent_id)
marker = f"signup_grant:{aid}"
if self.db.get_meta(marker) == "1":
return {"already": True, "account_id": aid, "balance": self.balance(aid)}
self.ledger.ensure_wallet(aid)
before = self.ledger._tx_seq
self.ledger.grant(aid, amount_credits, reason="signup-grant")
txs = [t for t in self.ledger.transactions if t.seq > before]
self._commit_accounts(aid, txs=txs)
self.db.set_meta(marker, "1")
self.db.commit()
return {
"already": False,
"account_id": aid,
"granted_credits": amount_credits,
"balance": self.balance(aid),
"tx": self.ledger.describe_tx(txs[-1]) if txs else None,
}
def charge_subscription(
self,
agent_id: str,
amount_credits: int = SUBSCRIPTION_CREDITS,
*,
cut_bps: int = SUBSCRIPTION_CUT_BPS,
) -> dict[str, Any]:
aid = agent_account(agent_id)
gross = to_subunits(amount_credits)
cut = cut_subunits(gross, cut_bps)
net = gross - cut
draws, tx_pay = self.ledger.purchase_settle(
aid,
ACCOUNT_COFFER,
gross,
reason="subscription:new-internet-seat",
tx_kind="subscription_charge",
meta={"agent_id": aid, "credits": amount_credits},
)
tx_fwd = self.ledger.transfer(
ACCOUNT_COFFER,
ACCOUNT_NEW_INTERNET,
net,
reason="subscription:forward-to-new-internet-co",
tx_kind="subscription_forward",
credit_kind=CreditKind.FUNDED,
meta={"agent_id": aid, "cut_centicredits": cut, "net_centicredits": net},
)
self._commit_accounts(
aid, ACCOUNT_COFFER, ACCOUNT_NEW_INTERNET, txs=[tx_pay, tx_fwd]
)
return {
"agent_id": aid,
"gross_credits": amount_credits,
"coffer_cut_credits": format_credits(cut),
"coffer_cut_centicredits": cut,
"leora_net_credits": format_credits(net),
"leora_net_centicredits": net,
"draws": [
{"kind": d["kind"], "amount_centicredits": d["amount"]} for d in draws
],
"txs": [self.ledger.describe_tx(tx_pay), self.ledger.describe_tx(tx_fwd)],
"balances": {
"agent": self.balance(aid),
"coffer": self.balance(ACCOUNT_COFFER),
"new_internet_co": self.balance(ACCOUNT_NEW_INTERNET),
},
}
def signup_flow(self, agent_id: str) -> dict[str, Any]:
wallet = self.create_wallet(agent_id)
grant = self.grant_signup(agent_id)
sub = self.charge_subscription(agent_id)
return {"wallet": wallet, "grant": grant, "subscription": sub}
def buy_shirt(
self,
agent_id: str,
quantity: int = 1,
idempotency_key: str | None = None,
) -> dict[str, Any]:
"""Order shirts with any credit. The price is held, not charged, until the operator
reviews the order (accept_order / reject_order)."""
return self.place_order(agent_id, "coffer-shirt", quantity, idempotency_key)
def place_order(self, buyer: str, sku: str, quantity: int = 1, idempotency_key: str | None = None) -> dict[str, Any]:
with self._atomic():
before = self.ledger._tx_seq
order = self.store.place_order(buyer, sku, quantity=quantity, idempotency_key=idempotency_key)
txs = [t for t in self.ledger.transactions if t.seq > before]
self._persist_order(order)
if idempotency_key:
self.db.execute("INSERT OR REPLACE INTO idempotency(key, order_id) VALUES(?,?)", (idempotency_key, order.order_id))
self._commit_accounts(order.buyer_id, ACCOUNT_ESCROW, txs=txs)
return self.order_view(order, txs)
def order_view(self, order, txs=None) -> dict[str, Any]:
out = {"order": asdict(order), "balance": self.balance(order.buyer_id)}
out["order"]["status_label"] = {"pending_review": "pending review", "rejected": "rejected, credits released",
"procuring": "accepted, printing", "fulfilled": "accepted"}.get(order.status, order.status)
if order.status == "pending_review":
out["notice"] = ORDER_REVIEW_NOTICE
if txs is not None:
out["txs"] = [self.ledger.describe_tx(t) for t in txs]
return out
def orders_for(self, buyer: str) -> list[dict[str, Any]]:
aid = human_account(buyer) if buyer.startswith("human:") else agent_account(buyer)
return [self.order_view(o)["order"] for o in self.store.orders.values() if o.buyer_id == aid]
def pending_orders(self) -> list[dict[str, Any]]:
"""For the operator: every order waiting for review, oldest first."""
rows = []
for o in sorted(self.store.pending_orders(), key=lambda o: int(o.order_id.split("-")[-1])):
row = self.db.execute("SELECT created_at FROM orders WHERE order_id=?", (o.order_id,)).fetchone()
rows.append({"order_id": o.order_id, "buyer": o.buyer_id, "sku": o.sku, "quantity": o.quantity,
"held_credits": o.held / SCALE, "submitted_at": row["created_at"] if row else None})
return rows
def _decide_order(self, method: str, order_id: str, actor: str, note: str) -> dict[str, Any]:
with self._atomic(): # reloads self.store, so look the method up inside
before = self.ledger._tx_seq
order = getattr(self.store, method)(order_id, actor, note=note, at=now_pt())
txs = [t for t in self.ledger.transactions if t.seq > before]
self._persist_order(order)
self._commit_accounts(order.buyer_id, ACCOUNT_ESCROW, self.store.merchant_account, txs=txs)
return self.order_view(order, txs)
def accept_order(self, order_id: str, actor: str, note: str = "") -> dict[str, Any]:
return self._decide_order("accept_order", order_id, actor, note)
def reject_order(self, order_id: str, actor: str, note: str = "") -> dict[str, Any]:
return self._decide_order("reject_order", order_id, actor, note)
def top_up(self, agent_id: str, amount_credits: int) -> dict[str, Any]:
aid = agent_account(agent_id)
before = self.ledger._tx_seq
self.ledger.top_up(aid, amount_credits)
txs = [t for t in self.ledger.transactions if t.seq > before]
self._commit_accounts(aid, txs=txs)
return {
"account_id": aid,
"balance": self.balance(aid),
"tx": self.ledger.describe_tx(txs[-1]),
}
# ----- people: wallets, the signup grant, and payments ---------------------
@contextlib.contextmanager
def _atomic(self):
"""One writer at a time across processes: BEGIN IMMEDIATE, reload from the DB, mutate,
commit (via _commit_accounts). On any error: roll back and reload."""
self.db.commit()
self.db.execute("BEGIN IMMEDIATE")
try:
self._load()
yield
except BaseException:
self.db._conn.rollback()
self._load()
raise
def _claim(self, key: str, request: str) -> str | None:
"""Insert an idempotency key first (inside _atomic). Returns the stored value if the key
already existed; raises if it existed for a different request."""
row = self.db.execute("SELECT order_id FROM idempotency WHERE key=?", (key,)).fetchone()
if row is not None:
stored = row["order_id"]
if stored.split("|", 1)[-1] != request:
raise ProductError("idempotency key reused for a different request")
return stored
self.db.execute("INSERT INTO idempotency(key, order_id) VALUES(?,?)", (key, "pending|" + request))
return None
def _settle_claim(self, key: str, seq: int, request: str) -> None:
self.db.execute("UPDATE idempotency SET order_id=? WHERE key=?", (f"{seq}|{request}", key))
def _tx_by_seq(self, seq: int) -> dict[str, Any]:
tx = next(t for t in self.ledger.transactions if t.seq == seq)
return self.ledger.describe_tx(tx)
def human_wallet(self, handle: str) -> dict[str, Any]:
return self.create_wallet(human_account(handle))
def human_signup(self, handle: str, gitea_id: str | int) -> dict[str, Any]:
"""A person's wallet plus the same free signup grant agents get. Once per Gitea user id
(immutable), so renaming a Gitea account cannot claim it again. Atomic across processes.
People pay no subscription seat."""
aid = human_account(handle)
gid = str(gitea_id or "")
if not gid.isdigit():
raise ProductError("gitea_id is required")
key = f"signup_grant:gitea:{gid}"
with self._atomic():
seen = self._claim(key, aid)
if seen is not None:
self.db.commit()
return {"already": True, "account_id": aid, "balance": self.balance(aid)}
self.ledger.ensure_wallet(aid)
self._ensure_account_row(aid)
before = self.ledger._tx_seq
self.ledger.grant(aid, SIGNUP_GRANT_CREDITS, reason="signup-grant")
txs = [t for t in self.ledger.transactions if t.seq > before]
self._settle_claim(key, txs[-1].seq, aid)
self._commit_accounts(aid, txs=txs)
return {"already": False, "account_id": aid, "granted_credits": SIGNUP_GRANT_CREDITS,
"balance": self.balance(aid), "tx": self.ledger.describe_tx(txs[-1])}
def pay(self, payer: str, payee: str, credits: float, *, reason: str, ref: str) -> dict[str, Any]:
"""Pay between people / a person and an agent, once per ref (e.g. an offer id). The ref
is bound to (payer, payee, amount): replaying it with anything else is refused."""
if not isinstance(ref, str) or not ref or len(ref) > 80 or "|" in ref:
raise ProductError("ref is required")
if isinstance(credits, bool) or not isinstance(credits, (int, float)) or not math.isfinite(credits):
raise ProductError("amount must be a finite number")
amount = round(float(credits) * SCALE)
if amount <= 0 or amount > MAX_PAYMENT_CREDITS * SCALE:
raise ProductError(f"amount must be between 0.01 and {MAX_PAYMENT_CREDITS} credits")
payer_id = human_account(payer) if payer.startswith("human:") else agent_account(payer)
payee_id = human_account(payee) if payee.startswith("human:") else agent_account(payee)
request = f"{payer_id}>{payee_id}:{amount}"
key = f"pay:{ref}"
with self._atomic():
seen = self._claim(key, request)
if seen is not None:
self.db.commit()
seq = seen.split("|", 1)[0]
if not seq.isdigit():
raise ProductError("payment in progress")
return {"duplicate": True, "tx": self._tx_by_seq(int(seq)), "balance": self.balance(payer_id)}
for aid in (payer_id, payee_id):
self.ledger.ensure_wallet(aid)
self._ensure_account_row(aid)
tx = self.ledger.pay(payer_id, payee_id, amount, (reason or "service")[:120], meta={"offer": ref})
self._settle_claim(key, tx.seq, request)
self._commit_accounts(payer_id, payee_id, txs=[tx])
return {"duplicate": False, "tx": self.ledger.describe_tx(tx), "balance": self.balance(payer_id)}
def all_balances(self) -> dict[str, dict]:
return {aid: self.balance(aid) for aid in sorted(self.ledger.accounts.keys())}
def public_snapshot(self) -> dict:
"""Full public ledger view (no human PII). All txs are public."""
return snapshot_from_product(self, now_pt())
def publish_public_ledger(self) -> dict:
"""Write public snapshot and publish to News (branch → PR → merge → surge)."""
snap = self.public_snapshot()
pub = Path(self.db.path).with_name("public-ledger.json")
import json
pub.write_text(
json.dumps(
{
"updated_at": snap["updated_at"],
"balances": snap["balances"],
"transactions": snap["transactions"],
},
indent=2,
sort_keys=True,
)
+ "\n",
encoding="utf-8",
)
from coffer.news_publish import try_auto_publish
result = try_auto_publish(snap["article_html"], updated_at=snap["updated_at"])
result["snapshot_path"] = str(pub)
return result
def _after_mutation(self) -> None:
"""Ledger changes are public; refresh local snapshot and News when enabled."""
import json
import os
snap = self.public_snapshot()
pub = Path(self.db.path).with_name("public-ledger.json")
pub.write_text(
json.dumps(
{
"updated_at": snap["updated_at"],
"balances": snap["balances"],
"transactions": snap["transactions"],
},
indent=2,
sort_keys=True,
)
+ "\n",
encoding="utf-8",
)
# Operator's to-do: orders waiting for review (account ids only).
pending = Path(self.db.path).with_name("pending-orders.json")
pending.write_text(json.dumps({"updated_at": snap["updated_at"], "pending": self.pending_orders()}, indent=2) + "\n",
encoding="utf-8")
if os.environ.get("COFFER_AUTO_PUBLISH_NEWS", "0") not in {"1", "true", "yes"}:
return
try:
from coffer.news_publish import try_auto_publish
try_auto_publish(snap["article_html"], updated_at=snap["updated_at"])
except Exception as exc: # noqa: BLE001
print(f"coffer news publish skipped: {exc}", flush=True)