# Commercial Platform — Domain Model

Status: **schema v3, frozen vocabulary.** Public API and BYO-provider work build
directly on the names below.

## Two products, sold separately

| id | name |
|---|---|
| `market_engine` | MarketEngine — prematch markets, canonical mapping, pricing, sellability |
| `live_intelligence` | Live Intelligence — live clock, actions, momentum, visualizer, timeline |

A client may hold either, both, or neither. There is no bundle entity: a bundle
is two subscriptions.

## Three entities, deliberately separate

```
client  ─── identity        who they are, how they authenticate, where they may embed
   │
   └── subscriptions ───── entitlement   which product, for which period
          │
          └── features ─── what the product grants, plus paid add-ons
```

**The client carries identity. The subscription carries the entitlement.** This
is the single most important decision in the model, and everything else follows
from it:

- one client can hold MarketEngine until Nov 1st and Live Intelligence until
  Nov 15th, and the first expiring does not switch off the second;
- renewing a subscription never changes the token;
- rotating the token never changes a subscription.

A single `client.expires_at` would collapse all three properties, which is why
it does not exist.

## Status versus expiry

| concept | belongs to | set by |
|---|---|---|
| `status` — `ACTIVE` · `SUSPENDED` · `REVOKED` | the client | a human decision |
| expiry — `expires_at` | the subscription | the passage of time |

They are kept apart so neither can mask the other. `SUSPENDED` and `REVOKED`
override every active subscription; expiry affects only its own product.
`REVOKED` is final — there is no reactivation and no hard delete, because the
audit trail of a deleted client would be deleted with it.

## Features and their parent

```
market_engine      premium_markets · pricing · canonical_mapping
live_intelligence  live_match · momentum · advanced_stats
```

A base subscription grants all of its product's features except
`advanced_stats`, which is sold as an add-on through `additional_features`.

**An add-on never applies while its parent product is inactive.** A client who
bought `advanced_stats` and let Live Intelligence lapse holds neither.

## The single authority

```js
resolveEntitlements(client, now)
  -> { status, products: { <product>: { active, expires_at, reason } },
       features: { <feature>: boolean } }
```

This is the only code permitted to decide whether a product is active, when it
expires, whether a feature is gated by its parent, or whether a status overrides
a subscription. No panel, route, gate or API layer re-derives any of it. Two
places deciding expiry drift apart; the gate asserts there is only one.

Reasons: `NO_SUBSCRIPTION` · `NOT_STARTED` · `EXPIRED` · `SUSPENDED` · `REVOKED`.
`NOT_STARTED` is distinct from `NO_SUBSCRIPTION` on purpose — a subscription
that begins tomorrow is not the absence of one.

## Frozen contracts

| | rule |
|---|---|
| C1 | A subscription is active when `starts_at <= now < expires_at`. The start is inclusive, the end is not, so two consecutive periods never overlap by a millisecond. |
| C2 | Every stored instant is ISO-8601 UTC with `Z`. |
| C3 | Overlapping periods for one product: active if any is active, and the reported `expires_at` is the maximum of the active ones (`null` means no end and wins). Renewal extends; it does not add a parallel period. |
| C4 | An add-on feature requires an active parent product. |
| C5 | `SUSPENDED`/`REVOKED` override every active subscription. |
| C6 | Rotation does not touch subscriptions; renewal does not touch the token. |
| C7 | `addSubscription`, `extendSubscription` and `rotateToken` accept an `operation_id`; the same id twice takes effect once. |
| C8 | No plaintext token in any log, audit entry or API response. |

C7 exists for one reason: a duplicated payment webhook must not add two months.

## Error codes

```
INVALID_API_TOKEN · CLIENT_SUSPENDED · CLIENT_REVOKED · SUBSCRIPTION_REQUIRED
FEATURE_NOT_ENABLED · ORIGIN_NOT_ALLOWED · PROVIDER_NOT_CONNECTED
```

## Audit events

```
CLIENT_CREATED · CLIENT_SUSPENDED · CLIENT_REACTIVATED · CLIENT_REVOKED
TOKEN_ROTATED · ORIGIN_ADDED · ORIGIN_REMOVED
SUBSCRIPTION_CREATED · SUBSCRIPTION_EXTENDED · FEATURE_ENABLED · FEATURE_DISABLED
```

`CLIENT_DELETED` is reserved and never emitted. It becomes usable once the audit
log is persisted outside the registry, because until then deleting a client
destroys the record that it existed.

Each entry: `timestamp · actor · event · client_id · before · after`.

## What this model is not

It is not billing. There are no plans, prices, invoices or payment states here.
Those arrive on top of this model and change none of it: a confirmed invoice
will do exactly one thing — call `extendSubscription` with an `operation_id`.

## Files

| file | role |
|---|---|
| `live-intelligence/clients.js` | registry, schema v3, `resolveEntitlements` |
| `live-intelligence/licensing.js` | the access gate (env license ∪ registry) |
| `live-intelligence/admin.js` | admin panel backend |
| `live-intelligence/migrate-registry-v3.js` | v1/v2 → v3 migration |
| `live-intelligence/klientet.js`, `licenca.js` | deprecated shims, no logic |

See also: [`client-registry.md`](client-registry.md),
[`subscriptions.md`](subscriptions.md), [`entitlements.md`](entitlements.md),
[`migration-v2-v3.md`](migration-v2-v3.md).
