"""@taxman-authored "Debits & credits" ledger — user-spec 2026-05-26. `log_tax_debit(user, slug)` appends one Line + spawns one Brief on the user's single TAX_LEDGER Post for each FREE/PAID DRAW spend at /gameboard/my-sea/. Parallels `apps.drama.models.Note.grant_if_new` for Note unlocks; the same post_save guard in `billboard.models` nukes any Line saved on a TAX_LEDGER Post w.o. admin_solicited=True. Two debit slugs today: free_draw_locked → my_sea_lock first-card-of-cycle paid_draw_locked → my_sea_paid_draw commit The Brief that spawns here is rendered via the existing slide-down banner (note.js `Brief.showBanner`); its FYI .btn-info navigates to the user's ledger Post; its NVM stamps the matching `User.{free,paid}_draw_brief_ dismissed_at` field via the dismiss-brief gameboard endpoints. Line text is timestamp-prefixed so a second identical-slug spend doesn't collide w. `Line.Meta.unique_together = ("post", "text")`.""" from django.utils import timezone from apps.billboard.models import ( Brief, Line, Post, TAX_LEDGER_POST_TITLE, ) from apps.lyric.models import get_or_create_taxman # Canonical Line text per slug. Replaces the prior `_showFreeDrawLockedBrief` # helper's wording in my_sea.html — the ledger Line IS the source of truth # for both the persistent log surface (Debits & credits Post) AND the slide- # down Brief banner (via Brief.line.text in to_banner_dict). # # `.btn-pri-name` spans wrap canonical .btn-primary button labels referenced # inline (FREE DRAW, PAID DRAW, GATE VIEW per user-spec 2026-05-26). SCSS # (`_billboard.scss` under `.post-line--system .post-line-text`) styles the # spans so the user sees the same `--quaUser` colour + 700-weight on the # token in prose as they would on the actual button. Rendered as HTML via # `Line.display_text|safe` in post.html (the system-author branch). TAX_DEBIT_TEMPLATES = { "free_draw_locked": ( 'Look!—My Sea\'s FREE DRAW ' 'is locked. Next free draw available 24h from the production of this log.' ), "paid_draw_locked": ( 'Look!—My Sea\'s PAID DRAW ' 'is locked. Another may be unlocked by depositing a Token in ' 'GATE VIEW.' ), } def log_tax_debit(user, slug): """Append a Line to the user's "Debits & credits" Post (creating the Post on first call) + spawn a Brief that the next page-load surfaces as a slide-down banner. Returns ``(post, line, brief)``. Raises ``KeyError`` for unknown slugs. Line text is prefixed with `[] ` so successive spends of the same slug produce distinct rows (each one survives `Line.Meta. unique_together = ("post", "text")`).""" if slug not in TAX_DEBIT_TEMPLATES: raise KeyError(f"Unknown tax debit slug: {slug!r}") post, _ = Post.objects.get_or_create( owner=user, kind=Post.KIND_TAX_LEDGER, defaults={"title": TAX_LEDGER_POST_TITLE}, ) # Existing TAX_LEDGER Posts (pre-feature migration) might lack a title; # heal once on next debit. Mirrors the Note.grant_if_new title heal. if post.title != TAX_LEDGER_POST_TITLE: post.title = TAX_LEDGER_POST_TITLE post.save(update_fields=["title"]) body = TAX_DEBIT_TEMPLATES[slug] # Sub-second timestamp prefix — keeps text unique even when two debits # land in the same wallclock second (defensive vs auto-draw paths that # could conceivably commit free + paid in rapid succession). stamp = timezone.now().isoformat(timespec="microseconds") text = f"[{stamp}] {body}" line = Line.objects.create( post=post, text=text, author=get_or_create_taxman(), admin_solicited=True, ) brief = Brief.objects.create( owner=user, post=post, line=line, kind=Brief.KIND_TAX_LEDGER, title=TAX_LEDGER_POST_TITLE, ) return post, line, brief