101 lines
3.9 KiB
Python
101 lines
3.9 KiB
Python
|
|
"""@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.html <span class="btn-pri-name">FREE DRAW</span> '
|
||
|
|
'is locked. Next free draw available 24h from the production of this log.'
|
||
|
|
),
|
||
|
|
"paid_draw_locked": (
|
||
|
|
'Look!—my_sea.html <span class="btn-pri-name">PAID DRAW</span> '
|
||
|
|
'is locked. Another may be unlocked by depositing a Token in '
|
||
|
|
'<span class="btn-pri-name">GATE VIEW</span>.'
|
||
|
|
),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
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 `[<ISO timestamp>] ` 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
|