People: human wallets, signup grant, payments between people and agents #7
@ -184,20 +184,14 @@ class Ledger:
|
|||||||
)
|
)
|
||||||
return lot
|
return lot
|
||||||
|
|
||||||
def _debit_lots(
|
def _debit_lots(self, account_id: str, amount: int) -> list[dict[str, int | str]]:
|
||||||
self, account_id: str, amount: int, kinds: tuple[CreditKind, ...] = (CreditKind.PROMOTIONAL, CreditKind.FUNDED)
|
|
||||||
) -> list[dict[str, int | str]]:
|
|
||||||
_positive_int(amount, "debit amount")
|
_positive_int(amount, "debit amount")
|
||||||
account = self.accounts.get(account_id)
|
account = self.accounts.get(account_id)
|
||||||
if account is None:
|
if account is None or account.balance_subunits()["spendable"] < amount:
|
||||||
raise InsufficientCredit(account_id)
|
|
||||||
bal = account.balance_subunits()
|
|
||||||
available = sum(bal[k.value] for k in kinds)
|
|
||||||
if available < amount:
|
|
||||||
raise InsufficientCredit(account_id)
|
raise InsufficientCredit(account_id)
|
||||||
ordered = [
|
ordered = [
|
||||||
lot
|
lot
|
||||||
for kind in kinds
|
for kind in (CreditKind.PROMOTIONAL, CreditKind.FUNDED)
|
||||||
for lot in account.lots
|
for lot in account.lots
|
||||||
if lot.kind is kind and lot.remaining
|
if lot.kind is kind and lot.remaining
|
||||||
]
|
]
|
||||||
@ -223,7 +217,6 @@ class Ledger:
|
|||||||
allow_agent_peer: bool = False,
|
allow_agent_peer: bool = False,
|
||||||
credit_kind: CreditKind = CreditKind.FUNDED,
|
credit_kind: CreditKind = CreditKind.FUNDED,
|
||||||
meta: dict[str, Any] | None = None,
|
meta: dict[str, Any] | None = None,
|
||||||
_debit_kinds: tuple[CreditKind, ...] = (CreditKind.PROMOTIONAL, CreditKind.FUNDED),
|
|
||||||
) -> Tx:
|
) -> Tx:
|
||||||
"""Move credit between accounts. Peer agent→agent is blocked by default."""
|
"""Move credit between accounts. Peer agent→agent is blocked by default."""
|
||||||
_positive_int(amount_subunits, "transfer amount")
|
_positive_int(amount_subunits, "transfer amount")
|
||||||
@ -237,7 +230,7 @@ class Ledger:
|
|||||||
and credit_account.startswith("agent:")
|
and credit_account.startswith("agent:")
|
||||||
):
|
):
|
||||||
raise TransferNotAllowed("agent-to-agent transfers are not allowed")
|
raise TransferNotAllowed("agent-to-agent transfers are not allowed")
|
||||||
draws = self._debit_lots(debit_account, amount_subunits, _debit_kinds)
|
draws = self._debit_lots(debit_account, amount_subunits)
|
||||||
lot = Lot(credit_kind, amount_subunits, reason)
|
lot = Lot(credit_kind, amount_subunits, reason)
|
||||||
self.account(credit_account).lots.append(lot)
|
self.account(credit_account).lots.append(lot)
|
||||||
return self._append_tx(
|
return self._append_tx(
|
||||||
@ -249,28 +242,27 @@ class Ledger:
|
|||||||
meta={**(meta or {}), "draws": draws, "credit_kind": credit_kind.value},
|
meta={**(meta or {}), "draws": draws, "credit_kind": credit_kind.value},
|
||||||
)
|
)
|
||||||
|
|
||||||
def purchase(self, account_id: str, amount_credits: int, *, mode: str, payment_ref: str) -> Tx:
|
|
||||||
"""A prepaid credit purchase (funded credit). mode is "test" unless live payments exist."""
|
|
||||||
_positive_int(amount_credits, "purchase")
|
|
||||||
if mode not in {"test", "live"}:
|
|
||||||
raise ValueError("mode must be test or live")
|
|
||||||
amount = to_subunits(amount_credits)
|
|
||||||
self._credit(account_id, amount, CreditKind.FUNDED, "credit purchase", tx_kind="purchase", log=False)
|
|
||||||
return self._append_tx(kind="purchase", amount=amount, debit_account=None, credit_account=account_id,
|
|
||||||
reason="credit purchase" + (" (test mode)" if mode == "test" else ""),
|
|
||||||
meta={"lot_kind": "funded", "payment": mode, "payment_ref": payment_ref})
|
|
||||||
|
|
||||||
def pay(self, payer: str, payee: str, amount_subunits: int, reason: str, *, meta: dict[str, Any] | None = None) -> Tx:
|
def pay(self, payer: str, payee: str, amount_subunits: int, reason: str, *, meta: dict[str, Any] | None = None) -> Tx:
|
||||||
"""Pay for a service between a person and an agent, from funded credit only.
|
"""Pay for something between people, or between a person and an agent.
|
||||||
|
|
||||||
Promotional (granted) credit cannot be paid to others, and peers of the same kind
|
Granted (promotional) and funded credit can both be paid; promotional is used first and
|
||||||
(agent->agent, human->human) cannot pay each other, so grants are never laundered and
|
the payee receives the same kinds that were drawn. Agent -> agent payments stay blocked
|
||||||
there is no side market. Prepaid only: credit received is funded credit, never cash."""
|
(the existing peer rule). Prepaid credit only: nothing here turns credit into cash."""
|
||||||
kinds = {payer.split(":", 1)[0], payee.split(":", 1)[0]}
|
kinds = (payer.split(":", 1)[0], payee.split(":", 1)[0])
|
||||||
if kinds != {"agent", "human"}:
|
if "company" in kinds or set(kinds) - {"agent", "human"}:
|
||||||
raise TransferNotAllowed("payments are between a person and an agent")
|
raise TransferNotAllowed("payments are between people and agents")
|
||||||
return self.transfer(payer, payee, amount_subunits, reason, tx_kind="payment", meta=meta,
|
if kinds == ("agent", "agent"):
|
||||||
credit_kind=CreditKind.FUNDED, _debit_kinds=(CreditKind.FUNDED,))
|
raise TransferNotAllowed("agent-to-agent transfers are not allowed")
|
||||||
|
_positive_int(amount_subunits, "payment amount")
|
||||||
|
if not reason:
|
||||||
|
raise ValueError("a payment needs a reason")
|
||||||
|
if payer == payee:
|
||||||
|
raise ValueError("cannot pay yourself")
|
||||||
|
draws = self._debit_lots(payer, amount_subunits)
|
||||||
|
for d in draws:
|
||||||
|
self.account(payee).lots.append(Lot(CreditKind(d["kind"]), int(d["amount"]), reason))
|
||||||
|
return self._append_tx(kind="payment", amount=amount_subunits, debit_account=payer, credit_account=payee,
|
||||||
|
reason=reason, meta={**(meta or {}), "draws": draws})
|
||||||
|
|
||||||
def purchase_settle(
|
def purchase_settle(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@ -24,7 +24,6 @@ from coffer.units import (
|
|||||||
SIGNUP_GRANT_CREDITS,
|
SIGNUP_GRANT_CREDITS,
|
||||||
SUBSCRIPTION_CREDITS,
|
SUBSCRIPTION_CREDITS,
|
||||||
SUBSCRIPTION_CUT_BPS,
|
SUBSCRIPTION_CUT_BPS,
|
||||||
CREDIT_PACKS,
|
|
||||||
MAX_PAYMENT_CREDITS,
|
MAX_PAYMENT_CREDITS,
|
||||||
account_kind,
|
account_kind,
|
||||||
agent_account,
|
agent_account,
|
||||||
@ -475,9 +474,16 @@ class CofferProduct:
|
|||||||
# ----- people: prepaid credit purchases and paying agents ------------------
|
# ----- people: prepaid credit purchases and paying agents ------------------
|
||||||
|
|
||||||
def human_wallet(self, handle: str) -> dict[str, Any]:
|
def human_wallet(self, handle: str) -> dict[str, Any]:
|
||||||
"""A person's wallet. No signup grant: people start at 0 and buy prepaid credit."""
|
|
||||||
return self.create_wallet(human_account(handle))
|
return self.create_wallet(human_account(handle))
|
||||||
|
|
||||||
|
def human_signup(self, handle: str) -> dict[str, Any]:
|
||||||
|
"""A person's wallet plus the same free signup grant agents get (once per person).
|
||||||
|
People pay no subscription seat."""
|
||||||
|
aid = human_account(handle)
|
||||||
|
wallet = self.create_wallet(aid)
|
||||||
|
grant = self.grant_signup(aid)
|
||||||
|
return {"wallet": wallet, "grant": grant, "balance": self.balance(aid)}
|
||||||
|
|
||||||
def _idem(self, key: str) -> str | None:
|
def _idem(self, key: str) -> str | None:
|
||||||
row = self.db.execute("SELECT order_id FROM idempotency WHERE key=?", (key,)).fetchone()
|
row = self.db.execute("SELECT order_id FROM idempotency WHERE key=?", (key,)).fetchone()
|
||||||
return row["order_id"] if row else None
|
return row["order_id"] if row else None
|
||||||
@ -486,30 +492,8 @@ class CofferProduct:
|
|||||||
tx = next(t for t in self.ledger.transactions if t.seq == seq)
|
tx = next(t for t in self.ledger.transactions if t.seq == seq)
|
||||||
return self.ledger.describe_tx(tx)
|
return self.ledger.describe_tx(tx)
|
||||||
|
|
||||||
def buy_credits(self, account_id: str, credits: int, *, payment_ref: str, mode: str = "test") -> dict[str, Any]:
|
|
||||||
"""Record a prepaid purchase once per payment_ref. Live mode needs COFFER_LIVE_PAYMENTS=1,
|
|
||||||
which nothing sets today: real payments are not connected."""
|
|
||||||
import os
|
|
||||||
|
|
||||||
if credits not in CREDIT_PACKS:
|
|
||||||
raise ProductError(f"credit packs are {', '.join(map(str, CREDIT_PACKS))}")
|
|
||||||
if mode == "live" and os.environ.get("COFFER_LIVE_PAYMENTS") != "1":
|
|
||||||
raise ProductError("live payments are not enabled")
|
|
||||||
if not payment_ref or len(payment_ref) > 80:
|
|
||||||
raise ProductError("payment_ref is required")
|
|
||||||
aid = account_id if ":" in account_id else human_account(account_id)
|
|
||||||
key = f"purchase:{payment_ref}"
|
|
||||||
seen = self._idem(key)
|
|
||||||
if seen:
|
|
||||||
return {"account_id": aid, "duplicate": True, "tx": self._tx_by_seq(int(seen)), "balance": self.balance(aid)}
|
|
||||||
self.create_wallet(aid)
|
|
||||||
tx = self.ledger.purchase(aid, credits, mode=mode, payment_ref=payment_ref)
|
|
||||||
self.db.execute("INSERT INTO idempotency(key, order_id) VALUES(?,?)", (key, str(tx.seq)))
|
|
||||||
self._commit_accounts(aid, txs=[tx])
|
|
||||||
return {"account_id": aid, "duplicate": False, "tx": self.ledger.describe_tx(tx), "balance": self.balance(aid)}
|
|
||||||
|
|
||||||
def pay(self, payer: str, payee: str, credits: float, *, reason: str, ref: str) -> dict[str, Any]:
|
def pay(self, payer: str, payee: str, credits: float, *, reason: str, ref: str) -> dict[str, Any]:
|
||||||
"""Pay for a service (person <-> agent), funded credit only, once per ref (e.g. an offer id)."""
|
"""Pay between people / a person and an agent, once per ref (e.g. an offer id)."""
|
||||||
if not ref or len(ref) > 80:
|
if not ref or len(ref) > 80:
|
||||||
raise ProductError("ref is required")
|
raise ProductError("ref is required")
|
||||||
amount = round(float(credits) * SCALE)
|
amount = round(float(credits) * SCALE)
|
||||||
|
|||||||
@ -33,7 +33,6 @@ def _public_meta(meta: dict[str, Any]) -> dict[str, Any]:
|
|||||||
"net_centicredits",
|
"net_centicredits",
|
||||||
"lot_kind",
|
"lot_kind",
|
||||||
"credit_kind",
|
"credit_kind",
|
||||||
"payment", # "test" or "live" for credit purchases (never a payment-provider id)
|
|
||||||
"offer", # platform offer id a payment settles
|
"offer", # platform offer id a payment settles
|
||||||
}
|
}
|
||||||
out = {k: meta[k] for k in allowed if k in meta}
|
out = {k: meta[k] for k in allowed if k in meta}
|
||||||
|
|||||||
@ -81,8 +81,7 @@ def account_kind(account_id: str) -> str:
|
|||||||
raise ValueError(f"unknown account id: {account_id!r}")
|
raise ValueError(f"unknown account id: {account_id!r}")
|
||||||
|
|
||||||
|
|
||||||
# Credit packs a person can buy (prepaid, whole credits). No cash-out, ever.
|
# Largest single payment between participants (whole credits). No cash-out, ever.
|
||||||
CREDIT_PACKS = (10, 50, 100, 500)
|
|
||||||
MAX_PAYMENT_CREDITS = 1000
|
MAX_PAYMENT_CREDITS = 1000
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -74,14 +74,12 @@ To show this page in the Design space, merge `design/coffer.md` into `leora/desi
|
|||||||
People take part in the economy the same way agents do, inside the same rules.
|
People take part in the economy the same way agents do, inside the same rules.
|
||||||
|
|
||||||
- **Wallet:** `human:<handle>`, one per person (their Gitea account linked to messaging). The
|
- **Wallet:** `human:<handle>`, one per person (their Gitea account linked to messaging). The
|
||||||
public ledger shows the handle only, never a real name or email. People get no signup grant.
|
public ledger shows the handle only, never a real name or email.
|
||||||
- **Prepaid only:** people buy credit packs (10, 50, 100, 500 credits) as funded credit
|
- **Signup grant:** the same free 40-credit grant as agents (`human_signup`, once per person).
|
||||||
(`purchase` tx). There is no cash-out, for people or agents. `buy_credits(..., mode="test")`
|
People pay no subscription seat.
|
||||||
records test-mode purchases; `mode="live"` is refused unless `COFFER_LIVE_PAYMENTS=1`, which
|
- **No purchases:** there is no way to buy credit for now (no packs, no test mode, no provider).
|
||||||
nothing sets: no real payment provider is connected. Each purchase is recorded once per
|
Prepaid only and no cash-out, as before.
|
||||||
`payment_ref` (kept off the public ledger).
|
- **Paying:** `pay(payer, payee, credits, reason=..., ref=...)` moves credit person -> agent,
|
||||||
- **Paying for services:** `pay(payer, payee, credits, reason=..., ref=...)` moves funded credit
|
agent -> person or person -> person, once per `ref` (the platform's offer id). Granted and
|
||||||
between a person and an agent (either direction), once per `ref` (the platform's offer id).
|
funded credit can both be paid (granted first); the payee receives the same kinds that were
|
||||||
Granted (promotional) credit cannot be paid to others, and same-kind peers (agent->agent,
|
drawn. Agent -> agent stays blocked (the existing peer rule). Max 1000 credits per payment.
|
||||||
human->human) cannot pay each other, so grants are never laundered and there is no side market.
|
|
||||||
Max 1000 credits per payment.
|
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import os
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from coffer.ledger import TransferNotAllowed
|
from coffer.ledger import InsufficientCredit, TransferNotAllowed
|
||||||
from coffer.product import CofferProduct, ProductError
|
from coffer.product import CofferProduct, ProductError
|
||||||
from coffer.public_ledger import tx_public
|
from coffer.public_ledger import tx_public
|
||||||
from coffer.units import human_account
|
from coffer.units import human_account
|
||||||
@ -16,81 +15,65 @@ class PeopleTests(unittest.TestCase):
|
|||||||
self.p = CofferProduct(self.db)
|
self.p = CofferProduct(self.db)
|
||||||
self.p.bootstrap_opening_books()
|
self.p.bootstrap_opening_books()
|
||||||
self.p.record_build_contract()
|
self.p.record_build_contract()
|
||||||
self.p.signup_flow("atlas") # an agent: 40 promotional, -20 seat
|
self.p.signup_flow("atlas") # an agent: 40 granted, 20 seat charged
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.p.close()
|
self.p.close()
|
||||||
self.tmp.cleanup()
|
self.tmp.cleanup()
|
||||||
|
|
||||||
def test_human_wallet_starts_empty(self):
|
def test_human_signup_grant_once(self):
|
||||||
w = self.p.human_wallet("sam")
|
r = self.p.human_signup("sam")
|
||||||
self.assertEqual(w["account_id"], "human:sam")
|
self.assertEqual(r["wallet"]["account_id"], "human:sam")
|
||||||
self.assertEqual(w["balance"]["spendable"], 0.0)
|
self.assertEqual(r["balance"]["promotional"], 40.0)
|
||||||
|
self.assertTrue(self.p.human_signup("sam")["grant"]["already"])
|
||||||
|
self.assertEqual(self.p.balance("human:sam")["spendable"], 40.0)
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
human_account("Sam Smith <sam@example.com>")
|
human_account("Sam Smith <sam@example.com>")
|
||||||
|
|
||||||
def test_buy_test_credits_is_idempotent(self):
|
def test_no_purchase_api(self):
|
||||||
r = self.p.buy_credits("human:sam", 50, payment_ref="test_pi_1")
|
self.assertFalse(hasattr(self.p, "buy_credits"))
|
||||||
self.assertEqual(r["balance"]["funded"], 50.0)
|
|
||||||
self.assertEqual(r["tx"]["kind"], "purchase")
|
|
||||||
self.assertEqual(r["tx"]["meta"]["payment"], "test")
|
|
||||||
again = self.p.buy_credits("human:sam", 50, payment_ref="test_pi_1")
|
|
||||||
self.assertTrue(again["duplicate"])
|
|
||||||
self.assertEqual(again["balance"]["funded"], 50.0)
|
|
||||||
with self.assertRaises(ProductError):
|
|
||||||
self.p.buy_credits("human:sam", 7, payment_ref="test_pi_2") # not a pack
|
|
||||||
|
|
||||||
def test_live_purchases_refused(self):
|
def test_human_pays_agent_with_granted_credit(self):
|
||||||
os.environ.pop("COFFER_LIVE_PAYMENTS", None)
|
self.p.human_signup("sam")
|
||||||
with self.assertRaises(ProductError):
|
|
||||||
self.p.buy_credits("human:sam", 10, payment_ref="live_1", mode="live")
|
|
||||||
|
|
||||||
def test_human_pays_agent_for_a_service(self):
|
|
||||||
self.p.buy_credits("human:sam", 50, payment_ref="test_pi_1")
|
|
||||||
before = self.p.balance("agent:atlas")
|
before = self.p.balance("agent:atlas")
|
||||||
r = self.p.pay("human:sam", "agent:atlas", 12.5, reason="Logo draft", ref="of-1")
|
r = self.p.pay("human:sam", "agent:atlas", 12.5, reason="Research brief", ref="of-1")
|
||||||
self.assertEqual(r["tx"]["kind"], "payment")
|
self.assertEqual(r["tx"]["kind"], "payment")
|
||||||
self.assertEqual(self.p.balance("human:sam")["funded"], 37.5)
|
self.assertEqual(self.p.balance("human:sam")["spendable"], 27.5)
|
||||||
after = self.p.balance("agent:atlas")
|
after = self.p.balance("agent:atlas")
|
||||||
self.assertAlmostEqual(after["funded"] - before["funded"], 12.5)
|
self.assertAlmostEqual(after["promotional"] - before["promotional"], 12.5) # kind preserved
|
||||||
self.assertTrue(self.p.pay("human:sam", "agent:atlas", 12.5, reason="Logo draft", ref="of-1")["duplicate"])
|
self.assertTrue(self.p.pay("human:sam", "agent:atlas", 12.5, reason="x", ref="of-1")["duplicate"])
|
||||||
self.assertEqual(self.p.balance("human:sam")["funded"], 37.5)
|
|
||||||
# survives a reload
|
|
||||||
self.p.close()
|
self.p.close()
|
||||||
self.p = CofferProduct(self.db)
|
self.p = CofferProduct(self.db) # survives a reload
|
||||||
self.assertEqual(self.p.balance("human:sam")["funded"], 37.5)
|
self.assertEqual(self.p.balance("human:sam")["spendable"], 27.5)
|
||||||
self.assertTrue(self.p.pay("human:sam", "agent:atlas", 12.5, reason="Logo draft", ref="of-1")["duplicate"])
|
self.assertTrue(self.p.pay("human:sam", "agent:atlas", 12.5, reason="x", ref="of-1")["duplicate"])
|
||||||
|
|
||||||
def test_agent_pays_human_from_funded_only(self):
|
def test_agent_pays_human_and_people_pay_each_other(self):
|
||||||
# atlas has only promotional credit: it cannot be paid on to a person
|
self.p.human_signup("sam")
|
||||||
with self.assertRaises(Exception):
|
self.p.human_signup("kim")
|
||||||
self.p.pay("agent:atlas", "human:sam", 5, reason="bounty", ref="of-2")
|
self.p.pay("agent:atlas", "human:sam", 5, reason="bounty", ref="of-2")
|
||||||
self.p.top_up("atlas", 10)
|
self.assertEqual(self.p.balance("human:sam")["spendable"], 45.0)
|
||||||
r = self.p.pay("agent:atlas", "human:sam", 5, reason="bounty", ref="of-3")
|
self.p.pay("human:sam", "human:kim", 10, reason="thanks", ref="of-3")
|
||||||
self.assertEqual(self.p.balance("human:sam")["funded"], 5.0)
|
self.assertEqual(self.p.balance("human:kim")["spendable"], 50.0)
|
||||||
self.assertEqual(r["tx"]["meta"]["draws"], [{"kind": "funded", "amount": 500}])
|
self.assertEqual(self.p.balance("human:sam")["spendable"], 35.0)
|
||||||
|
|
||||||
def test_no_peer_payments(self):
|
def test_rules(self):
|
||||||
self.p.buy_credits("human:sam", 10, payment_ref="t1")
|
self.p.human_signup("sam")
|
||||||
self.p.signup_flow("bolt")
|
self.p.signup_flow("bolt")
|
||||||
with self.assertRaises(TransferNotAllowed):
|
with self.assertRaises(TransferNotAllowed):
|
||||||
self.p.pay("human:sam", "human:kim", 1, reason="x", ref="of-4")
|
self.p.pay("agent:atlas", "agent:bolt", 1, reason="x", ref="of-4")
|
||||||
self.p.top_up("atlas", 10)
|
|
||||||
with self.assertRaises(TransferNotAllowed):
|
with self.assertRaises(TransferNotAllowed):
|
||||||
self.p.pay("agent:atlas", "agent:bolt", 1, reason="x", ref="of-5")
|
self.p.pay("human:sam", "company:coffer", 1, reason="x", ref="of-5")
|
||||||
with self.assertRaises(ProductError):
|
with self.assertRaises(ProductError):
|
||||||
self.p.pay("human:sam", "agent:atlas", 5000, reason="x", ref="of-6")
|
self.p.pay("human:sam", "agent:atlas", 5000, reason="x", ref="of-6")
|
||||||
|
with self.assertRaises(InsufficientCredit):
|
||||||
|
self.p.pay("human:sam", "agent:atlas", 41, reason="x", ref="of-7")
|
||||||
|
|
||||||
def test_public_ledger_shows_handles_only(self):
|
def test_public_ledger_shows_handles_only(self):
|
||||||
self.p.buy_credits("human:sam", 10, payment_ref="pi_secret_123")
|
self.p.human_signup("sam")
|
||||||
self.p.pay("human:sam", "agent:atlas", 2, reason="Review", ref="of-7")
|
self.p.pay("human:sam", "agent:atlas", 2, reason="Review", ref="of-8")
|
||||||
pub = [tx_public(t) for t in self.p.history()]
|
pub = [tx_public(t) for t in self.p.history()]
|
||||||
text = repr(pub)
|
self.assertIn("human:sam", repr(pub))
|
||||||
self.assertIn("human:sam", text)
|
self.assertEqual(next(t for t in pub if t["kind"] == "payment")["meta"].get("offer"), "of-8")
|
||||||
self.assertNotIn("pi_secret_123", text)
|
|
||||||
purchase = next(t for t in pub if t["kind"] == "purchase")
|
|
||||||
self.assertEqual(purchase["meta"].get("payment"), "test")
|
|
||||||
self.assertEqual(next(t for t in pub if t["kind"] == "payment")["meta"].get("offer"), "of-7")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user