Skip to content

feat(core): entitlement state machine + public-render resolver (spec §6) - #17

Merged
OriginDevIT merged 1 commit into
mainfrom
feat/core-entitlement-sm
Aug 30, 2026
Merged

feat(core): entitlement state machine + public-render resolver (spec §6)#17
OriginDevIT merged 1 commit into
mainfrom
feat/core-entitlement-sm

Conversation

@OriginDevIT

Copy link
Copy Markdown
Owner

Closes #9

New packages/core — pure domain logic, no database, no clock, no I/O.

transition(status, trigger) — §6.3

(EntitlementStatus, EntitlementTrigger) → { from, to, emits }, or throws IllegalTransitionError. Every illegal (status, trigger) pair is rejected, never silently ignored. Triggers carry only what the table branches on — trial configured?, payment succeeded?, override target.

The deliberate emit asymmetries in the table are preserved:

Transition Emits Why
active --refund--> expired listing.tier_changed only immediate, no entitlement.expired per §6.3
grace --30d--> expired entitlement.expired only listing already downgraded at grace
active --term end--> / canceled --period end--> expired entitlement.expired + listing.tier_changed

Emitted-event names are sourced from @osds/adapter-kit (EntitlementEventType minus renewal_due — a scheduled notice, not a transition — plus listing.tier_changed).

resolvePublicRender(status) — §6.5

EntitlementStatus → { badge, featuredPlacement, perks, ownerNotice }. past_due keeps badge + featured slot + full perks (most failed payments are involuntary); grace/expired drop to rank-0. none follows §6.2 (rank-0, nothing to show).

Tests — vitest, 157

  • one assertion per §6.3 row, in table order
  • admin override legal from every state; override to a non-status rejected
  • ~12 explicit illegal cases, each asserting IllegalTransitionError carrying from / trigger
  • a full 8 × 14 (status × trigger) matrix — every cell is either the expected accept or a reject
  • one per §6.5 row, plus none

Config

  • passWithNoTests removed from vitest.config.ts (chore: remove passWithNoTests once real tests exist #9). **/dist/** excluded so the tsc --build *.test.js copies don't double-run (they did — 4 files / 314 tests before the exclude).
  • root tsconfig.json references packages/core.

pnpm typecheck / lint / test all pass.

Note

§6.3 has 16 data rows; the task said 17. I couldn't find a 17th in v0.4 — every row is covered, and I read it as approximate. Flagging in case a row was expected that isn't in the spec.

🤖 Generated with Claude Code

New packages/core. Pure domain logic - no database, no clock, no I/O.

- transition(status, trigger) encodes the §6.3 table: returns
  { from, to, emits } or throws IllegalTransitionError. Illegal
  (status, trigger) pairs are rejected, never silently ignored. Triggers
  carry only what the table branches on (trial configured? payment
  succeeded? override target). The deliberate emit asymmetries are
  preserved: refund -> expired emits only listing.tier_changed;
  grace -> expired emits only entitlement.expired (listing already
  downgraded); term/cancel -> expired emit both.
- resolvePublicRender(status) encodes the §6.5 table: badge, featured
  placement, perk level, owner notice. past_due keeps badge + featured +
  full perks; grace/expired drop to rank-0. `none` follows §6.2.
- Emitted-event names are sourced from @osds/adapter-kit
  (EntitlementEventType minus renewal_due, plus listing.tier_changed).

Tests (vitest, 157): one per §6.3 row in table order, admin override
from every state, ~12 explicit illegal cases asserting
IllegalTransitionError with from/trigger, a full 8x14 (status x trigger)
matrix asserting every cell is either the expected accept or a reject,
and one per §6.5 row plus `none`.

Removes passWithNoTests from vitest.config.ts now that tests exist, and
excludes **/dist/** so the tsc-emitted *.test.js copies don't double-run.
Closes #9.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Puf9jhj137U3iswf4GWZkG
Signed-off-by: Matthew Wren <info@origindev.com>
@OriginDevIT
OriginDevIT merged commit eb76770 into main Aug 30, 2026
1 check passed
@OriginDevIT
OriginDevIT deleted the feat/core-entitlement-sm branch August 30, 2026 21:03
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.

chore: remove passWithNoTests once real tests exist

1 participant