"""@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