# Timed Seat Grants

> Canonical path: `specs/saas/timed-seat-grants.md`
> Status: draft-spec
> Scope: 平台 operator 手动给组织赠送有期限 human seats 的 MVP；
> grant 严格属于 organization-level entitlement。

Related canonical specs:

- [Organization Seat Purchases](../billing/organization-seat-purchases.md)
- [AgentTeam Admin Dashboard Spec](../admin/dashboard.md)
- [organization Specs](../organization/README.md)
- [Specs And Guardrails Layout](../repository/specs-and-guardrails-layout.md)

## 1. 目的

AgentTeam 需要一个克制的 SaaS entitlement MVP：平台 operator 可以给某个客户组织
**人工赠送固定数量、固定有效期的团队成员坐席**。这些坐席只增加
organization-level human-seat capacity，不改变付费订阅、订单、支付或成员身份。

第一版只回答三个问题：

1. 如何记录一笔可审计、可撤销、会过期的人工赠送；
2. 如何把当前 active grant 叠加进既有组织 human-seat quota；
3. 过期或撤销导致 over-limit 时，如何保持现有成员不变并阻止新增 seat。

活跃等级、活动达标、自动发放、通知和统计都不是本 MVP。

## 2. MVP 范围

### 2.1 目标

- 平台 operator 可为 organization 创建 `N` 个有期限 human seats。
- `expires_at` 必填；`effective_at` 可选，缺省为创建时间。
- 支持 grant create / list / revoke；创建请求重试时不会重复发放。
- active grants 计入现有 organization human-seat limit。
- Seat summary 能展示 Free / Paid / Gift breakdown、最近 grant 到期时间和
  over-limit 状态。
- Invite/member activation 继续复用既有 quota check 与 quota-exceeded 路径。

### 2.2 非目标

- 不做 campaign、活跃等级、自动评估、进度页、领取/报名、rollup、转化统计或通知。
- 不做 workspace-local seat capacity，也不限制赠送 seat 只能在某 workspace 使用。
- 不写入或修改付费订阅、订单、支付、发票或退款记录。
- 不把历史 `organization_quota_overrides` 或
  `organization_seat_entitlement_grants` 当作运行时 source of truth。
- 不在过期/撤销后自动移除成员、降级成员或取消 pending invites。
- 不设计通用 entitlement engine；第一版只解决 organization human seats。

## 3. 关键口径

- Seat scope：赠送 seat 是 organization-level capacity；grant model 不包含
  workspace ownership 或 workspace-local attribution。
- 时间：数据库存储和比较统一使用 UTC；API 边界使用 RFC3339；有效区间为
  `[effective_at, expires_at)`。
- 过期：`now >= expires_at` 后自然不再计入 quota；quota 正确性不能依赖定时任务。
- 撤销：soft revoke 原 row，不删除，不写负数 adjustment row。
- Paid stacking：gifted seats 与 Free seats、active paid subscription seats 叠加。
- Grant source：运行时读取 `organization_resource_quota_grants`。历史迁移把
  `organization_quota_overrides` 和 `organization_seat_entitlement_grants`
  数据转换到该表；fresh database 不创建旧表，升级库可为兼容保留旧表，
  但运行时不再读取旧表。

## 4. Grant ledger

Grant ledger table：

```text
organization_resource_quota_grants
```

MVP 字段保持在实现必需范围内：

| 字段 | 规则 |
| --- | --- |
| `id` | Primary key。 |
| `organization_id` | 必填；grant 目标组织。 |
| `metric` | human seat grant 固定为 `seats`。 |
| `quota_scope` | human seat grant 固定为 `organization`。 |
| `limit_delta` | 必填正整数；撤销不写负数行。 |
| `expires_at` | 可为空；为空表示永久 grant。非空时过期由时间派生。 |
| `source_type` | MVP 固定为 `operator_manual`。 |
| `source_ref` | 可选，记录 support ticket / sales note 等外部引用。 |
| `request_id` | 创建流程生成并在同一次提交的重试中复用；不由运营填写，按 organization + metric + scope 唯一。 |
| `reason` | 必填，operator 可读的发放原因。 |
| `created_by_operator` | 必填，平台 operator。 |
| `revoked_at` / `revoked_by_operator` / `revoke_reason` | revoke 时写入。 |
| `created_at` / `updated_at` | 必填。 |

建议约束：

- `UNIQUE (organization_id, metric, quota_scope, request_id)`，从数据库层阻止同一次提交请求被重试成多笔 grant。
- 支持按 organization 查询 active grants 的索引。
- FK 到 organization。
- DB 限制 `limit_delta > 0`；应用层限制有效期、`reason`、`source_ref`
  的长度/上限。

`request_id` 只解决传输层重复提交，例如双击、超时重试或刷新后重放同一次请求。
它不尝试判断“运营是否真的想再发一笔相同赠送”；这类误操作由后台预览、近期记录提示和撤销能力降低风险。

`metadata_json` 若实现需要，可以作为 bounded optional context；quota 计算不得依赖它。
MVP 不加入 campaign、rule、tier、progress、rollup 或 conversion-stat 字段。

## 5. Quota 行为

Active grant contribution：

```text
grant_human_seats = SUM(limit_delta)
  WHERE organization_id = :organization_id
    AND metric = 'seats'
    AND quota_scope = 'organization'
    AND revoked_at IS NULL
    AND (expires_at IS NULL OR now < expires_at)
```

Effective limit：

```text
effective_human_seat_limit = free_human_seats
  + active_paid_human_seats
  + grant_human_seats
```

要求：

- 老调用方继续读取兼容的总 limit 字段。
- 新 summary 返回 Free / Paid / Gift breakdown、`nextGrantExpiresAt` 和
  `humanSeatsOverLimit`。
- 客户侧 summary 不暴露 operator-only 字段，例如内部 reason、operator id、敏感
  `source_ref`。
- Create/revoke 与 invite/member activation 的并发处理应沿用现有 quota 串行化策略；
  如果需要新增锁，优先使用 organization-level entitlement lock。

## 6. Admin 能力与展示

MVP 只要求平台后台具备三类能力，具体路由和页面形态遵循现有 admin 约定：

### 6.1 Create grant

- 需要 platform-operator 权限。
- 请求包含 `seatDelta`、`expiresAt`、必填 `reason`，可选 `effectiveAt`、
  `sourceRef`。
- Create 请求带创建流程生成的 `requestId`；前端/后台在同一次提交的重试中复用它。
  同一 organization 内同 `requestId` 同 payload 返回原 grant，不同 payload 返回 conflict。
  这个 ID 不出现在运营表单里。
- 返回 created grant 和创建后的 compact seat summary。

### 6.2 Operator UX

后台表单面向运营人员，避免暴露底层字段：

- 运营只需要选择 organization、填写赠送 seats、到期时间、发放原因，可选填写工单/备注。
- 提交前展示预览：当前 seats、赠送后总 limit、最近到期时间。
- 系统自动生成并复用防重复 request ID；运营不需要理解或填写。
- 同一组织近期已有相似 grant 时，提交前给出提醒，但不做硬拦截。
- 撤销时展示撤销后的 seats/limit 预览；如果会 over-limit，必须明确提示但不自动移除成员。

### 6.3 List grants

- 需要 platform-operator 权限。
- 支持按 organization 查看 grant 历史。
- 每条记录返回 stored state 与 computed status：`scheduled`、`active`、`expired`、
  `revoked`。
- 列表只用于后台操作和审计，不改变 quota 语义。

### 6.4 Revoke grant

- 需要 platform-operator 权限。
- `reason` 必填。
- 更新原 row 的 revoke 字段；重复 revoke 返回当前 revoked state 或 existing
  already-revoked domain response。
- 返回 revoked grant 和撤销后的 compact seat summary。
- 如果撤销导致 over-limit，不移除成员，只在 summary/UI 中明确标记。

### 6.5 客户侧展示

客户侧不新增 mutation API。Billing/Seats、People/Invite 或现有 quota error UI 只需解释：

- 当前 human seats 与总 limit；
- Free / Paid / Gift breakdown；
- active granted seat count 或最近 grant 到期时间；
- over-limit 时需要购买 seat、等待/申请续赠，或移除成员后才能新增 human seat。

不做活动卡片、进度条、领取按钮、统计面板或复杂通知。

## 7. 权限、审计与隐私

- Grant mutation 仅限 platform operator；organization admin 只能看到 redacted effect。
- Create/revoke 至少要能追溯 actor、target organization、seat delta、
  effective/expires timestamps、reason、source reference、request ID，以及 revoke
  actor/time/reason。
- 如果仓库已有 admin audit/event 机制，create/revoke 应写入；否则 ledger row 是 MVP
  最低限度的 durable audit record。
- `reason`、`source_ref`、可选 metadata 默认只在后台展示；客户 UI 使用中性文案。

## 8. 过期、撤销与 over-limit

Grant 过期或撤销后，组织可能出现：

```text
current_human_seats > effective_human_seat_limit
```

必须行为：

- 已有 organization members 保持不变。
- Pending invites 不自动取消；但接受邀请若会新增 human seat，必须走既有
  quota-exceeded 失败路径。
- Seat summary 返回 `humanSeatsOverLimit = true`。
- 新增 human seat 被阻止，直到购买、续赠/补赠或移除成员后恢复 headroom。

## 9. 必要失败路径

| 场景 | MVP 行为 |
| --- | --- |
| operator 权限缺失 | 按 admin unauthorized/forbidden 拒绝。 |
| 缺少系统生成的 `requestId` | validation error；后台 UI 正常不应发生。 |
| 同 `requestId` 同 payload 重试 | 返回原 grant 和 summary。 |
| 同 `requestId` 不同 payload | conflict，不创建第二条 grant。 |
| 新 `requestId` 但内容相似 | 不自动拦截；后台提示近期相似 grant，运营确认后可继续。 |
| organization 不存在 | not found。 |
| `seatDelta <= 0` | validation error。 |
| `expiresAt <= effectiveAt` 或创建时已过期 | validation error。 |
| 超过 ops policy 的 seats/有效期 | validation error。 |
| grant 已 revoked | 返回当前 revoked state 或 already-revoked domain response。 |
| 过期/撤销导致 over-limit | 不移除成员；summary 标记 over-limit；阻止新增 human seat。 |

## 10. 实现验证清单

首版测试/验收聚焦最小闭环：

- Ledger 校验：创建重试不重复发放、撤销、必填 reason、有效期和 seat 上限。
- Resolver 校验：Free、Paid、scheduled、active、expired、revoked。
- Quota flow 校验：active grant 提供 headroom；过期/撤销后新增 seat 失败且已有成员保留。
- Admin API/UI 校验：create/list/revoke 权限、运营表单预览、summary breakdown、revoke over-limit 提示。
- Customer display 校验：不暴露后台审计字段，能解释 gift seats 和 over-limit。

## 11. 发布与运维边界

发布可以分步：先加 ledger 与 resolver，再开放 operator create/list/revoke，最后补后台和客户侧
summary 文案。回滚优先关闭新增 grant；既有 grant 通过 revoke 或自然过期收敛。不要把移除
resolver contribution 当作常规回滚手段，因为这可能让客户立即 over-limit。

MVP guardrails：强制 reason、系统生成 request ID、单次最大 seats、最长有效期；后台必须用预览和提示降低运营误操作风险。

## 12. 未来演进说明

未来活跃激励可以作为新的 producer 写入同一 grant ledger，例如使用新的 `source_type` 和确定性
`request_id`。活动配置、规则状态、activity rollup、dry-run/evaluate API、客户进度 UI、统计、
反作弊和 eligibility filters 都应另写 spec；它们不进入 timed manual grant MVP。

## 13. 待确认问题

1. 严格 organization-level capacity 是否满足“团队成员坐席”的产品口径？
2. 赠送 seats 是否始终与 Free、paid seats 叠加？
3. 过期/撤销后是否确认只阻止新增 seat，不自动移除成员或 pending invites？
4. 是否需要为超大 seat grant 增加额外审批或二次确认？
5. 客户侧是否只显示“赠送坐席数 + 到期时间”，不显示 source reference？
6. 已 expired grant 是否允许再 revoke 以补审计标签，还是返回 already-final？
7. Billing disabled deployment 中，gift quota 应在哪些成员管理 UI 中可见？
