126 lines
5.3 KiB
Python
126 lines
5.3 KiB
Python
import unittest
|
|
|
|
from coffer.ledger import CreditKind, InsufficientCredit, Ledger
|
|
from coffer.units import to_subunits
|
|
|
|
L, O = "agent:leora", "company:orbit"
|
|
|
|
|
|
def ledger(promo, funded):
|
|
lg = Ledger()
|
|
if promo:
|
|
lg.credit_subunits(L, to_subunits(promo), CreditKind.PROMOTIONAL, "signup-grant")
|
|
if funded:
|
|
lg.credit_subunits(L, to_subunits(funded), CreditKind.FUNDED, "top-up")
|
|
return lg
|
|
|
|
|
|
class FundedOnlyTest(unittest.TestCase):
|
|
def test_40_promo_60_funded_cannot_invest_100(self):
|
|
lg = ledger(40, 60)
|
|
n = len(lg.transactions)
|
|
with self.assertRaises(InsufficientCredit):
|
|
lg.transfer(L, O, to_subunits(100), "investment:x", tx_kind="investment")
|
|
b = lg.account(L).balance_subunits()
|
|
self.assertEqual(b["promotional"], 4000)
|
|
self.assertEqual(b["funded"], 6000)
|
|
self.assertEqual(len(lg.transactions), n)
|
|
|
|
def test_contract_payment_is_funded_only(self):
|
|
lg = ledger(40, 60)
|
|
with self.assertRaises(InsufficientCredit):
|
|
lg.transfer(L, O, to_subunits(70), "c", tx_kind="contract_payment")
|
|
|
|
def test_investment_draws_funded_only_and_leaves_promo(self):
|
|
lg = ledger(40, 200)
|
|
tx = lg.transfer(L, O, to_subunits(100), "investment:x", tx_kind="investment")
|
|
self.assertTrue(all(d["kind"] == "funded" for d in tx.meta["draws"]))
|
|
b = lg.account(L).balance_subunits()
|
|
self.assertEqual(b["promotional"], 4000)
|
|
self.assertEqual(b["funded"], 10000)
|
|
|
|
def test_plain_transfer_keeps_promo_first(self):
|
|
lg = ledger(40, 60)
|
|
tx = lg.transfer(L, O, to_subunits(50), "t")
|
|
self.assertEqual(tx.meta["draws"][0], {"kind": "promotional", "amount": 4000})
|
|
|
|
def test_reversal_restores_original_draw_kinds(self):
|
|
lg = ledger(40, 60)
|
|
from unittest import mock
|
|
# Simulate the pre-fix code path that produced ledger seq 39 (promo drawn).
|
|
with mock.patch("coffer.ledger.FUNDED_ONLY_TX_KINDS", frozenset()):
|
|
bad = lg.transfer(L, O, to_subunits(100), "inv", tx_kind="investment")
|
|
self.assertEqual(bad.meta["draws"][0]["kind"], "promotional")
|
|
revs = lg.reverse_tx(bad, "correction:inv")
|
|
self.assertEqual([(t.kind, t.amount, t.meta["credit_kind"]) for t in revs],
|
|
[("investment_reversal", 4000, "promotional"),
|
|
("investment_reversal", 6000, "funded")])
|
|
self.assertTrue(all(t.meta["reverses_seq"] == bad.seq for t in revs))
|
|
self.assertTrue(all((t.debit_account, t.credit_account) == (O, L) for t in revs))
|
|
b = lg.account(L).balance_subunits()
|
|
self.assertEqual((b["promotional"], b["funded"]), (4000, 6000))
|
|
self.assertEqual(lg.account(O).balance_subunits()["spendable"], 0)
|
|
with self.assertRaises(ValueError):
|
|
lg.reverse_tx(bad, "again")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|
|
|
|
|
|
class FundedOnlyGuardsTest(unittest.TestCase):
|
|
def test_cannot_opt_out_of_funded_only(self):
|
|
from coffer.ledger import TransferNotAllowed
|
|
for kind in ("investment", "contract_payment"):
|
|
lg = ledger(40, 200)
|
|
with self.assertRaises(TransferNotAllowed):
|
|
lg.transfer(L, O, to_subunits(100), "x", tx_kind=kind, funded_only=False)
|
|
self.assertEqual(lg.account(L).balance_subunits()["promotional"], 4000)
|
|
|
|
def test_reversal_of_reversal_refused(self):
|
|
lg = ledger(0, 100)
|
|
tx = lg.transfer(L, O, to_subunits(100), "inv", tx_kind="investment")
|
|
rev = lg.reverse_tx(tx, "fix")[0]
|
|
with self.assertRaises(ValueError):
|
|
lg.reverse_tx(rev, "fix again")
|
|
|
|
|
|
class ProductReverseAtomicTest(unittest.TestCase):
|
|
def setUp(self):
|
|
import tempfile, os
|
|
from coffer.product import CofferProduct
|
|
self.d = tempfile.TemporaryDirectory()
|
|
os.environ["COFFER_AUTO_PUBLISH_NEWS"] = "0"
|
|
self.path = f"{self.d.name}/c.db"
|
|
self.p = CofferProduct(self.path)
|
|
self.p.ledger.credit_subunits(L, 10000, CreditKind.FUNDED, "top-up", log=False)
|
|
tx = self.p.ledger.transfer(L, O, 10000, "inv", tx_kind="investment")
|
|
self.p._commit_accounts(L, O, txs=[tx])
|
|
self.seq = tx.seq
|
|
|
|
def tearDown(self):
|
|
self.p.close(); self.d.cleanup()
|
|
|
|
def test_reverse_persists(self):
|
|
from coffer.product import CofferProduct
|
|
self.p.reverse_transaction(self.seq, "fix")
|
|
q = CofferProduct(self.path)
|
|
self.assertEqual(q.balance(L)["funded_centicredits"], 10000)
|
|
self.assertEqual(q.balance(O)["spendable_centicredits"], 0)
|
|
self.assertEqual(q.ledger.history()[-1].meta["reverses_seq"], self.seq)
|
|
q.close()
|
|
|
|
def test_failed_save_rolls_back(self):
|
|
from unittest import mock
|
|
from coffer.product import CofferProduct
|
|
n = len(self.p.ledger.history())
|
|
with mock.patch.object(self.p, "_persist_tx", side_effect=RuntimeError("disk")):
|
|
with self.assertRaises(RuntimeError):
|
|
self.p.reverse_transaction(self.seq, "fix")
|
|
self.assertEqual(len(self.p.ledger.history()), n)
|
|
self.assertEqual(self.p.balance(O)["spendable_centicredits"], 10000)
|
|
q = CofferProduct(self.path)
|
|
self.assertEqual(len(q.ledger.history()), n)
|
|
self.assertEqual(q.balance(O)["spendable_centicredits"], 10000)
|
|
q.close()
|