Skip to content

1.30 backend: block allowance — 10 included + one-time packs - #38

Merged
tetakta merged 2 commits into
mainfrom
session/1.30-block-quota
Oct 15, 2026
Merged

tetakta merged 2 commits into
mainfrom
session/1.30-block-quota

Conversation

@tetakta

@tetakta tetakta commented Oct 15, 2026

Copy link
Copy Markdown
Member

Summary

  • "Everything free, pay only for extra blocks" (owner, 2026-10-15): +10 included forever, one-time purchasable packs (+10 $7, +100 $49, +1000 $149), never expire.
  • New append-only block_entitlements table (migration 017, same no-UPDATE/DELETE trigger pattern as admin_audit_log) records every grant; a correction is a new compensating row, never an edit.
  • Effective allowance = settings.included_block_allowance (10, named constant) + sum of grants, computed on read (routes/blocks.py::block_allowance/block_usage), never stored on businesses.
  • Enforced only on POST /businesses/{id}/blocks: over allowance → 402 with a machine-readable {message, used, allowance, next_pack} body, never a bare 403. PATCH/DELETE/reorder are untouched.
  • GET /businesses/{id} gains owner-only blocks_used/blocks_allowance (None for every other caller) — /preview, /proof, by-slug/public use separate response shapes that never carry these fields, so there's no leak path to close there.
  • Admin comp path: POST /admin/entities/{id}/grant-blocks (require_admin), writes the entitlement + the mandatory admin_audit_log row. No payment code here — Stripe is session 1.31.
  • Prod measured before shipping (via tetapi-psql): 36 entities, 21 blocks total, max 4 blocks on any single entity, zero above 10 — ships with no grandfathering needed, ahead of the 455-entity GTM batch.

Note on numbering: boot called this 1.30 (branch kept as given), but that number was already taken on main by the bitcoin_confirmed fix (PR #36/#37) — same collision pattern as roadmap rows 1.24→1.25/1.25→1.26. The infra roadmap/changelog entry for this work is 1.32.

Test plan

  • tests/test_block_quota.py (16 unit tests, stub-session style matching test_device_revoke.py): 10th block OK / 11th → 402; pack_100 grant unblocks the 111th, blocks the 112th-equivalent; PATCH/DELETE/reorder ignore quota; BusinessOut quota fields default None; admin grant writes both rows; unknown source → 422.
  • tests/test_blocks_is_public.py updated to stub the new quota check (unrelated to that suite's own is_public assertions).
  • CI (pytest -q, full dependency install) — this sandbox's local dependency install didn't complete in a reasonable time (native builds); relying on the GitHub Actions runner, which installs .[dev] fresh per .github/workflows/tests.yml.
  • Post-deploy: verify the real 402 on a scratch entity via the test key, and that the append-only trigger rejects an UPDATE on block_entitlements (can only be proven against real Postgres).

🤖 Generated with Claude Code

tetakta and others added 2 commits October 15, 2026 12:02
…r cap)

Owner, 2026-10-15: "everything free, pay only for extra blocks" (+10 $7,
+100 $49, +1000 $149, one-time, permanent, never expire). Adds append-only
block_entitlements (migration 017, same no-UPDATE/DELETE trigger as
admin_audit_log) and enforces settings.included_block_allowance (10) + sum
of grants on POST /businesses/{id}/blocks only — 402 with a machine-readable
{used, allowance, next_pack} body, never a bare 403. PATCH/DELETE/reorder
untouched. GET /businesses/{id} gains owner-only blocks_used/blocks_allowance
(None elsewhere) — every public/preview/proof/by-slug payload already uses a
separate schema, so there's no leak path to close there.

Admin comp path: POST /admin/entities/{id}/grant-blocks (require_admin),
writes the entitlement + the mandatory admin_audit_log row. No payment code
here — Stripe is session 1.31.

Prod measured before shipping (tetapi-psql): 36 entities, 21 blocks total,
max 4 on any single entity, zero above 10 — ships with no grandfathering
needed, ahead of the 455-entity GTM batch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI caught it: the two "allowed" quota tests hit add_block's real
db.refresh(block, ["media"]) call after flush, which _StubSession didn't
implement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tetakta
tetakta merged commit e3db487 into main Oct 15, 2026
1 check passed
@tetakta
tetakta deleted the session/1.30-block-quota branch October 15, 2026 20:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant