# ADP PTO 同步 - 数据模型

## 概览

| 模型 | Schema | 用途 |
|---|---|---|
| `User`（修改） | `platform_iam` | 加 `adpAoid` + `adpLinkedAt` 字段（外部 ID 模式） |
| `AdpPtoSchedule`（新增） | `platform_automation` | PTO 时段镜像 |
| `AutomationTask`（复用） | `platform_automation` | 任务定义（code: `ADP_LINKER` / `ADP_PTO_SYNC`） |
| `AutomationExecution`（复用） | `platform_automation` | 每次同步执行记录 |
| `AutomationTaskType`（修改） | `platform_automation` | 加 `ADP_SYNC` 枚举值 |

## User 字段补充

```prisma
model User {
  // ... 现有字段 ...
  adpAoid       String?    @unique @map("adp_aoid") @db.VarChar(64)
  adpLinkedAt   DateTime?  @map("adp_linked_at") @db.Timestamptz(3)

  adpPtoSchedules AdpPtoSchedule[]
}
```

| 字段 | 含义 |
|---|---|
| `adpAoid` | ADP `associateOID`，PTO API 的主键。由 linker 任务按 email 匹配后填入。可空（员工还没匹配上 / 不在 ADP）。`@unique` 防重。 |
| `adpLinkedAt` | linker 写入 aoid 的时间，运维可见性。 |

**外部 ID 模式**：与 Entra 的 `externalId` / `externalSource` 一致。User 表只挂"外部 ID"，不挂业务镜像表。

## AdpPtoSchedule（新表）

```prisma
model AdpPtoSchedule {
  id          String    @id @default(cuid()) @db.VarChar(32)
  userId      String    @map("user_id") @db.Uuid
  adpAoid     String    @map("adp_aoid") @db.VarChar(64)
  leaveDate   DateTime  @map("leave_date") @db.Date
  startTime   DateTime  @map("start_time") @db.Timestamptz(3)
  endTime     DateTime  @map("end_time") @db.Timestamptz(3)
  adpEntryId  String    @map("adp_entry_id") @db.VarChar(128)
  syncedAt    DateTime  @default(now()) @map("synced_at") @db.Timestamptz(3)

  user        User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([userId, adpEntryId])
  @@index([userId, leaveDate])
  @@index([startTime, endTime])
  @@map("adp_pto_schedules")
  @@schema("platform_automation")
}
```

### 字段语义

| 字段 | 含义 |
|---|---|
| `userId` | 关联本系统 User（含级联删除）。每条记录必有 user。 |
| `adpAoid` | 冗余存 ADP associateOID。即使 User 解绑也能反查 ADP 来源。 |
| `leaveDate` | 请假**日期**（不含时间）。一天可能有多个 entry（上午+下午分别请）。 |
| `startTime` / `endTime` | 该 entry 的实际起止时间（含时区）。**会议时段重叠判断的基准**。 |
| `adpEntryId` | ADP 端 timeOffEntry 的不透明 ID。仅用于 upsert/delete 幂等锚点。 |
| `syncedAt` | 最近一次同步该行的时间。 |

### 隐私合规：禁止字段

**不得**新增以下字段（违反 CPRA 健康信息保护与目的限定）：

- ❌ `leaveType` / `payCode` / `policyCode`
- ❌ `reason` / `comment` / `note`
- ❌ `requestorName` / `submitterName`
- ❌ `status`（cancelled/pending 走硬删除策略）

如未来必须扩展，先与法务对齐。

### 索引

| 索引 | 用途 |
|---|---|
| `(userId, adpEntryId)` UNIQUE | upsert 幂等 |
| `(userId, leaveDate)` | "某员工某天有 PTO 吗" 高频查询 |
| `(startTime, endTime)` | 会议时段重叠扫描 |

## AutomationTask 注册

启动时 upsert 两条：

```typescript
{ code: 'ADP_LINKER',   type: 'ADP_SYNC', scheduleType: 'CRON', config: { cron: '0 2 * * *' }  }
{ code: 'ADP_PTO_SYNC', type: 'ADP_SYNC', scheduleType: 'CRON', config: { cron: '30 2 * * *' } }
```

未匹配 email 数计入 `AutomationExecution.result.recordsUnmatched`，详情入 `AutomationExecution.logs`。

## 数据生命周期

| 数据 | 创建 | 更新 | 删除 |
|---|---|---|---|
| `User.adpAoid` | linker 首次匹配成功 | 重新匹配（少见） | 用户离职 → User 删除 → null |
| `AdpPtoSchedule` | PTO sync 发现新 entry | 时段变更 | ADP cancel/pending → **硬删** / 落出窗口 → 不动 |

## 与现有 `MeetingAttendanceLeaveRecord` 的区别

| | `MeetingAttendanceLeaveRecord` | `AdpPtoSchedule` |
|---|---|---|
| 来源 | 人工录入 | ADP API |
| 类型字段 | 有 (`type: LeaveType`, `reason`) | **无** |
| 隐私级别 | 人工知情 | 仅"是否不在/何时不在" |
| 用途 | 既有功能保留 | 新增 PTO 自动标记 |

**两表并存，互不影响**。
