"""ITs for `apps.billboard.tax.log_tax_debit` — the @taxman-authored "Debits & credits" ledger (user-spec 2026-05-26). Mirrors the shape of `apps.drama.tests.integrated.test_note_brief` for `Note.grant_if_new` — each spend appends a Line + spawns a Brief on the user's single TAX_LEDGER Post.""" from django.test import TestCase from apps.billboard.models import Brief, Line, Post, TAX_LEDGER_POST_TITLE from apps.billboard.tax import ( TAX_DEBIT_TEMPLATES, log_tax_debit, ) from apps.lyric.models import User, get_or_create_taxman class LogTaxDebitTest(TestCase): def setUp(self): self.user = User.objects.create(email="tax@test.io") def test_free_draw_locked_creates_post_line_brief(self): post, line, brief = log_tax_debit(self.user, "free_draw_locked") # Post created with the canonical title + correct kind self.assertEqual(post.owner, self.user) self.assertEqual(post.kind, Post.KIND_TAX_LEDGER) self.assertEqual(post.title, TAX_LEDGER_POST_TITLE) # Line authored by @taxman + admin_solicited self.assertEqual(line.post, post) self.assertEqual(line.author, get_or_create_taxman()) self.assertTrue(line.admin_solicited) # Brief points at the Post + Line w. correct kind self.assertEqual(brief.owner, self.user) self.assertEqual(brief.post, post) self.assertEqual(brief.line, line) self.assertEqual(brief.kind, Brief.KIND_TAX_LEDGER) self.assertEqual(brief.title, TAX_LEDGER_POST_TITLE) def test_paid_draw_locked_uses_paid_template_text(self): _, line, _ = log_tax_debit(self.user, "paid_draw_locked") self.assertIn("PAID DRAW", line.text) # `GATE VIEW` is wrapped in a `.btn-pri-name` span for inline styling # parity w. the actual button label (user-spec 2026-05-26), so the # raw text has HTML between "depositing a Token in " + "GATE VIEW". self.assertIn("depositing a Token in", line.text) self.assertIn("GATE VIEW", line.text) def test_free_draw_locked_uses_free_template_text(self): _, line, _ = log_tax_debit(self.user, "free_draw_locked") self.assertIn("FREE DRAW", line.text) self.assertIn("24h from the production of this log", line.text) def test_two_spends_share_one_post_with_two_lines(self): """Like Note unlocks: one Post per user, growing thread of Lines.""" log_tax_debit(self.user, "free_draw_locked") log_tax_debit(self.user, "paid_draw_locked") posts = Post.objects.filter(owner=self.user, kind=Post.KIND_TAX_LEDGER) self.assertEqual(posts.count(), 1) self.assertEqual(posts.first().lines.count(), 2) def test_two_free_draws_produce_distinct_lines(self): """Each spend produces a UNIQUE Line — text is timestamp-prefixed so a second identical-slug spend doesn't collide with `unique_together = (post, text)`.""" log_tax_debit(self.user, "free_draw_locked") log_tax_debit(self.user, "free_draw_locked") post = Post.objects.get(owner=self.user, kind=Post.KIND_TAX_LEDGER) line_texts = list(post.lines.values_list("text", flat=True)) self.assertEqual(len(line_texts), 2) self.assertNotEqual(line_texts[0], line_texts[1]) def test_unknown_slug_raises(self): with self.assertRaises(KeyError): log_tax_debit(self.user, "no_such_slug") def test_template_keys_cover_both_known_slugs(self): self.assertIn("free_draw_locked", TAX_DEBIT_TEMPLATES) self.assertIn("paid_draw_locked", TAX_DEBIT_TEMPLATES) class UnsolicitedLineGuardTest(TestCase): """post_save guard nukes any Line saved on a TAX_LEDGER Post w.o. admin_solicited=True — mirrors the NOTE_UNLOCK defense-in-depth.""" def setUp(self): self.user = User.objects.create(email="guard@test.io") self.taxman = get_or_create_taxman() self.post = Post.objects.create( owner=self.user, kind=Post.KIND_TAX_LEDGER, title=TAX_LEDGER_POST_TITLE, ) def test_unsolicited_line_on_tax_ledger_gets_deleted(self): Line.objects.create( post=self.post, text="impostor", author=self.taxman, admin_solicited=False, ) self.assertEqual(self.post.lines.count(), 0) def test_solicited_line_on_tax_ledger_survives(self): Line.objects.create( post=self.post, text="legit", author=self.taxman, admin_solicited=True, ) self.assertEqual(self.post.lines.count(), 1) class TaxmanReservedUsernameTest(TestCase): """`taxman` joins `adman` as a reserved system-author handle.""" def test_taxman_is_reserved(self): from apps.lyric.models import is_reserved_username self.assertTrue(is_reserved_username("taxman")) self.assertTrue(is_reserved_username("TAXMAN")) # case-insensitive def test_get_or_create_taxman_is_idempotent(self): a = get_or_create_taxman() b = get_or_create_taxman() self.assertEqual(a.pk, b.pk) self.assertEqual(a.email, "taxman@earthmanrpg.local")